TypeScript SDK quick guide

Emit, consume, and run the control round-trip from TypeScript, over the schema-generated types.

@agenteventprotocol/sdk provides emit/consume/control helpers over the schema-generated envelope and payload types (README). File paths below are relative to the typescript-sdk repository root.

It's a local, unpublished package: private: true in package.json until the v0.1 tag, so you use it from a clone, not npm install @agenteventprotocol/sdk.

This guide covers the same three tasks the smoke test exercises: emit, consume, control round-trip.

Setup

The package has zero runtime dependencies and targets Node ≥ 22 (global fetch/WebSocket) or any runtime with those. From a clone, either build a local package to depend on (npm install && npm run build && npm pack, per the README's install section), or import directly by relative path as the smoke test does:

ts
import { Emitter, wsTransport, subscribe, ControlClient } from '../src/index.js';

(src/index.ts is the public export surface; everything below is re-exported from there.) Typecheck with npm run typecheck (strict, no emit; see the script in package.json). Run the end-to-end smoke test with bash test/run-smoke.sh; it boots the vendored relay test fixture itself (test/smoke.ts is the source of every snippet below).

Emit an event

Emitter builds valid envelopes for one producer identity; call .session() once per session to get a SessionEmitter that owns that session's (epoch, seq) counter (src/emit.ts):

ts
import { Emitter, wsTransport } from '@agenteventprotocol/sdk';

const t = wsTransport('http://127.0.0.1:8787', { agent: 'my-agent', host: 'host-1' });
const emitter = new Emitter({ agent: 'my-agent', host: 'host-1', sink: t.sink, epoch: 1 });
const session = emitter.session('s_001');

session.emit('session.started', { client: { name: 'my-agent' } });

epoch is a constructor option you own: bump it whenever the process restarts without durable seq state, per AEP-0001 §7. sink is anywhere finished Events go: wsTransport() for a duplex connection to the relay (needed if you'll also receive control commands; see below), or httpTransport() for a simple POST-per-Event path (src/transports.ts). emit() on a SessionEmitter is typed against AepPayloadMap for registered types, so session.emit('tool.completed', {...}) gets payload-shape checking at compile time.

Consume / subscribe

subscribe() opens an SSE connection with attr-match filtering, dedupes by id, and tracks (session, epoch, seq) resume positions for you (src/consume.ts):

ts
import { subscribe } from '@agenteventprotocol/sdk';

const sub = subscribe({
  url: 'http://127.0.0.1:8787',
  filter: { severity: { gte: 'notice' } },
  onEvent: (ev) => console.log(ev.type),
});

// later, to persist for a clean resume:
const positions = sub.positions(); // ResumePosition[]; feed back as `from` next time
sub.close();

The filter is the attr-match dialect. positions() gives you exactly what to persist and replay as from on reconnect; see Write a consumer for why resume-by-position and dedupe-by-id both matter.

Control round-trip

ControlClient is the sending side of the experimental control profile. It opens the WS duplex, sends a command, and resolves on the correlated control.accepted (or rejects with NackError on control.rejected or a locally synthesized ack-window timeout); see src/control.ts:

ts
import { ControlClient, NackError } from '@agenteventprotocol/sdk';

const ctl = new ControlClient({ url: 'http://127.0.0.1:8787', agent: 'phone', host: 'host-1' });

try {
  const ack = await ctl.send({
    type: 'control.attention.respond',
    session: 's_001',
    subject: requestId,       // the attention.requested id
    cause: requestId,
    data: { answer: { option: 'allow' } },
  }, { ackWindowMs: 5000, retries: 1 });
  console.log('accepted', ack.id);
} catch (e) {
  if (e instanceof NackError) console.error(e.reason, e.detail);
}

Retries reuse the same command id (idempotency; see AEP-0004 §2.3): a retried send never double-executes on the target side. A target that still remembers the command answers the retry by re-emitting its recorded ack byte-identically (§3), so a lost ack is recovered by the retry itself. On the receiving side (an emitter accepting commands), pass controlAccepts to wsTransport() and handle inbound commands via onCommand; see the target-side block in test/smoke.ts, which acks with control.accepted/control.rejected and then emits the outcome (attention.answered, attention.resolved).

The three helper areas

graph TD
    E["Emitter / SessionEmitter\n(emit.ts)"] --> S[sink: httpTransport or wsTransport]
    Sub["subscribe()\n(consume.ts)"] --> SSE[SSE stream]
    CC["ControlClient\n(control.ts)"] --> WS[WS duplex]
    S --> R[relay]
    SSE --> R
    WS --> R

See also

On this page