TypeScript client for the Agent Host Protocol (AHP).
Browser-friendly client built on the global WebSocket API. Works in
modern browsers and Node 21+ without additional runtime dependencies.
The package exposes four subpath exports:
| Import path | What it gives you |
|---|---|
@microsoft/agent-host-protocol |
Wire types, actions, commands, reducers, version constants. No I/O. |
@microsoft/agent-host-protocol/client |
AhpClient, Subscription, AhpStateMirror, the AhpTransport interface, InMemoryTransport, and the error taxonomy. |
@microsoft/agent-host-protocol/hosts |
MultiHostClient, HostClientHandle, ReconnectPolicy, ClientIdStore (with InMemoryClientIdStore), MultiHostStateMirror, and the Host*Error family. Builds on /client to manage one or more host connections with reconnect, generation-checked handles, and fan-in events. |
@microsoft/agent-host-protocol/ws |
WebSocketTransport — an AhpTransport implementation backed by the global WebSocket. |
The split mirrors the Rust SDK (ahp-types, ahp, ahp::hosts,
ahp-ws) — wire types and reducers are decoupled from the client,
which is in turn decoupled from a specific transport and from the
multi-host orchestration layer.
import { ActionType, type ActionEnvelope } from '@microsoft/agent-host-protocol';
import { AhpClient, AhpStateMirror } from '@microsoft/agent-host-protocol/client';
import { WebSocketTransport } from '@microsoft/agent-host-protocol/ws';
const transport = await WebSocketTransport.connect('ws://localhost:12345');
const client = new AhpClient(transport);
const mirror = new AhpStateMirror();
client.connect();
const init = await client.initialize({
clientId: 'my-client',
protocolVersions: ['0.3.0'],
initialSubscriptions: ['ahp-root://'],
});
for (const snapshot of init.snapshots) {
mirror.applySnapshot(snapshot);
}
const root = client.attachSubscription('ahp-root://');
(async () => {
for await (const event of root) {
if (event.type === 'action') mirror.apply(event.params);
}
})();
const sessionUri = `ahp-session:/${crypto.randomUUID()}`;
client.dispatch(sessionUri, {
type: ActionType.SessionTurnStarted,
// … remaining action fields
} as unknown as ActionEnvelope['action']);AhpClient is transport-agnostic. Any framed message stream — a
WebSocket, a Unix socket, stdio, or an in-memory pair for tests — can
back an AhpTransport:
import type { AhpTransport, TransportFrame, JsonRpcMessage } from '@microsoft/agent-host-protocol/client';
class MyTransport implements AhpTransport {
send(message: JsonRpcMessage | string): void { /* … */ }
async recv(): Promise<TransportFrame | null> { /* … */ }
close(): void { /* … */ }
}InMemoryTransport.pair() returns two connected halves that exchange
text frames — handy for unit tests that don't need a real socket.
The reducer functions (rootReducer, sessionReducer,
terminalReducer, changesetReducer) are pure: replaying actions in
serverSeq order on any prior snapshot yields identical state. This
is the same property the Rust and Swift clients rely on for
reconnection.
AhpStateMirror is a convenience that holds one RootState, a
Map<URI, SessionState>, a Map<URI, TerminalState>, and a
Map<URI, ChangesetState>. Apply Snapshots and ActionEnvelopes and
it keeps those maps up to date. Larger apps usually keep their own
state and call the reducers directly.
| Class | When it's thrown |
|---|---|
RpcError |
JSON-RPC error response from the server. Carries code, message, data. |
RpcTimeoutError |
Client-side timeout fired before the server responded. Carries method, timeoutMs. Distinct from RpcError. |
TransportError |
Failure of the underlying transport. kind: 'closed' | 'io' | 'protocol'. |
ClientClosedError |
Request was in flight when the client was shut down. |
AhpClientError |
Base class for every error this SDK throws — use instanceof to catch them all. |
Malformed inbound frames don't throw — they're logged via console.warn and the channel stays alive (matching the Rust client's tracing::warn! behavior). Pending requests still time out via RpcTimeoutError if the dropped frame would have been their reply.
Some AHP methods (currently resourceRequest) can be initiated by the
server. By default, the client responds with JSON-RPC MethodNotFound
so the server does not leak a pending request. Install a typed handler
to take over:
client.setServerRequestHandler(async (method, params) => {
if (method === 'resourceRequest') {
return { /* … */ };
}
throw new RpcError(JsonRpcErrorCodes.MethodNotFound, 'unhandled');
});AhpClient.reconnect(...) sends the typed AHP reconnect request on
an already-open transport. It does not decide when to reconnect, how
often to retry, whether authentication errors are terminal, or how to
update UI while reconnecting — those policies live in the app.
A typical app-level reconnect flow is:
- Open a fresh transport and
AhpClient. - Attach event streams before the handshake.
- Call
connect()andreconnect({ clientId, lastSeenServerSeq, subscriptions }). - Apply the returned replay actions or snapshots to your app store.
- Re-fetch
listSessionsor other ephemeral data — protocol notifications are not replayed.
If you'd rather not write that loop yourself, see
@microsoft/agent-host-protocol/hosts —
it ships a MultiHostClient that owns the reconnect supervisor,
re-subscribes across reconnects, mirrors root state, and exposes
generation-checked client handles. Single-host consumers use
MultiHostClient.single(...).
For apps that talk to one or more AHP hosts at once (a local
sessions server plus a tunnel-attached remote, multiple project hosts
in a sidebar, …), the @microsoft/agent-host-protocol/hosts entry
point ships MultiHostClient:
import { ActionType } from '@microsoft/agent-host-protocol';
import { WebSocketTransport } from '@microsoft/agent-host-protocol/ws';
import {
MultiHostClient,
type HostTransportFactory,
} from '@microsoft/agent-host-protocol/hosts';
const openLocal: HostTransportFactory = async (_hostId, _signal) =>
WebSocketTransport.connect('ws://localhost:12345');
// Single-host: same API, never see "registry" concepts.
const { multi, host } = await MultiHostClient.single({
id: 'local',
label: 'Local sessions server',
transportFactory: openLocal,
});
console.log(`connected to ${host.label}: ${host.state.status}`);
// Multi-host: add as many as you need.
await multi.addHost({
id: 'tunnel',
label: 'Tunnel',
transportFactory: async (_id, _signal) =>
WebSocketTransport.connect('wss://my-tunnel.example/sessions'),
});
// Fan-in of every inbound event, tagged with host of origin.
for await (const event of multi.events()) {
console.log(`[${event.hostId}] ${event.channel}`, event.event.type);
}Each host runs its own reconnect supervisor with the configured
ReconnectPolicy (defaults to exponential backoff from 250 ms to 30 s
with 25 % jitter), re-subscribes to known URIs after reconnects, and
mirrors root state plus a session-summary cache so MultiHostClient .aggregatedSessions() and aggregatedAgents() are snapshot reads,
not fan-out subscriptions. Every successful (re)connect bumps a per-
host generation counter; HostClientHandles minted at an earlier
generation throw HostReconnectedError instead of silently writing to
the new connection.
Persistent clientIds are pluggable via the ClientIdStore
interface. The default InMemoryClientIdStore is session-stable;
production apps that need cross-launch identity wrap their platform's
secure storage (localStorage, IndexedDB, Node fs,
safeStorage, …) in a custom ClientIdStore.
For multi-host state, MultiHostStateMirror keys per-resource state
by (hostId, uri) so URIs that legitimately collide across hosts
(the normal case for session URIs) don't clobber each other.
The wire types under src/types/ are generated from types/*.ts at the
repository root and are not committed to the repo — avoiding a
byte-for-byte duplication of the canonical TypeScript sources. Regenerate
them whenever you pull or change the protocol:
npm run generate:typescript # from the repo rootGenerated files carry a banner; do not edit them by hand. The
generate:typescript script is also part of npm run generate, which
regenerates every language's client output.
This package exports two protocol-version constants from its default entry point:
import { PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS } from '@microsoft/agent-host-protocol';-
PROTOCOL_VERSION— SemVer string for the version this package's source tree implements. -
SUPPORTED_PROTOCOL_VERSIONS— every version this package is willing to negotiate (most-preferred-first). Pass it asprotocolVersionsonInitializeParams:await client.initialize({ clientId: 'my-client', protocolVersions: [...SUPPORTED_PROTOCOL_VERSIONS], initialSubscriptions: ['ahp-root://'], });
The same information is mirrored, in machine-readable form, in
release-metadata.json and, in human-readable
form, in CHANGELOG.md. CI verifies all three sources
agree on every PR.
From a fresh checkout:
# 1. Install the root tooling and generate the TS client's wire types.
npm install
npm run generate:typescript
# 2. Work in the client package.
cd clients/typescript
npm install
npm run typecheck
npm test
npm run buildCI runs the generate step automatically before the install/typecheck/test/build sequence, so contributors only need to remember step 1 locally after pulling protocol changes.
MIT