SDK architecture: TypeScript and Python

Architecture of the TypeScript and Python SDKs providing emit, consume, and control layers over generated types.

The code this page cites lives in the SDK repositories, agenteventprotocol/typescript-sdk and agenteventprotocol/python-sdk; sdk/ts/ and sdk/python/ paths below name each SDK repository's source tree.

Both SDKs share the same three-layer shape over the same generated types, kept in parity on purpose: sdk/ts/ (zero dependencies, Node ≥ 22) and sdk/python/ (one dependency, pydantic>=2, Python ≥ 3.10). Both publish from their own repositories (see each package's README).

Layering

At the bottom, sdk/ts/src/gen/aep-types.ts and sdk/python/aep_sdk/gen/aep_types.py are generated from schemas/ by schemas/codegen/generate.py; see repo-map.md for the full spec→schemas→codegen chain. Neither file is hand-edited: each generated file's first line says DO NOT EDIT (sdk/ts/src/gen/aep-types.ts:1, sdk/python/aep_sdk/gen/aep_types.py:1), and each repo's CI regenerates from the pinned spec commit and fails on any diff (sdk/ts/README.md:73-76, sdk/python/README.md:120-123).

Hand-written modules sit on top of the generated types, matched concern-for-concern across languages (the Python SDK additionally ships aep_sdk/aio.py, an asyncio mirror of the consume surface and of the control-sender contract):

Concern TypeScript Python
Emit sdk/ts/src/emit.ts (Emitter/SessionEmitter) sdk/python/aep_sdk/emit.py (Emitter/SessionEmitter)
Consume sdk/ts/src/consume.ts (subscribe()) sdk/python/aep_sdk/consume.py (subscribe())
Control sdk/ts/src/control.ts (ControlClient/ControlTarget) sdk/python/aep_sdk/control.py (ControlSender/ControlTarget)

Emitter/SessionEmitter construct valid envelopes and own a session's (epoch, seq) counter (sdk/ts/src/emit.ts:38-110, sdk/python/aep_sdk/emit.py:33-126), the same ownership contract AEP-0001 §7 requires of any emitter, whether it's this SDK, an adapter, or aep-mcp. subscribe() wraps the SSE binding with attr-match filtering, id-dedupe, and resume-position tracking built in, not left to the caller (sdk/ts/src/consume.ts:1-4, 34-109), the same consumer conformance relay.md and cli.md describe for the relay and CLI.

graph TD
    schemas["schemas/ (aep-event.schema.json,\nregistry/, types/)"] --> codegen["schemas/codegen/generate.py"]
    codegen --> tstypes["sdk/ts/src/gen/aep-types.ts"]
    codegen --> pytypes["sdk/python/aep_sdk/gen/aep_types.py"]
    tstypes --> tsemit["emit.ts"]
    tstypes --> tsconsume["consume.ts"]
    tstypes --> tscontrol["control.ts"]
    pytypes --> pyemit["emit.py"]
    pytypes --> pyconsume["consume.py"]
    pytypes --> pycontrol["control.py"]
    tsemit --> app["your application"]
    tsconsume --> app
    tscontrol --> app
    pyemit --> app
    pyconsume --> app
    pycontrol --> app

The control state machine

control.ts's ControlClient and control.py's ControlSender are the same state machine, the control-as-events design priced into ~250 lines in TypeScript and ~140 in Python.

send() builds a command envelope (addressing the session, omitting seq, since commands aren't buffered events, per AEP-0004 §2.2), transmits it, and resolves when a control.accepted/control.rejected arrives correlated by cause = command.id. A window timeout (default 10s) synthesizes a local timeout nack if nothing comes back. Retries resend the identical envelope with the same id, so a target that dedupes on id never re-executes a retried command (sdk/ts/src/control.ts:143-191, sdk/python/aep_sdk/control.py:86-114).

The two implementations differ only where the transport forces them to. The TS ControlClient owns its own WebSocket connection and subscribes to ack types itself (sdk/ts/src/control.ts:64-135), because a browser/Node runtime has WebSocket built in. The sync Python ControlSender is transport-agnostic (the caller injects a send callback and feeds inbound events to on_event()), because the standard library has no WebSocket client (sdk/python/aep_sdk/control.py:1-5).

The asyncio module closes that gap with a hand-rolled, dependency-free duplex: aio.ControlClient owns its WebSocket connection (RFC 6455 client side on asyncio.open_connection, sdk/python/aep_sdk/aio.py) and mirrors the same send() contract with the same NackError, so both languages serve both worlds.

Both also ship ControlTarget, the target-side helper (dedupe-on-id, exactly one ack per deduplicated command, unsupported nack for undeclared types) implementing the target-side conformance rules from AEP-0004 §7. The TS one mirrors the Python original and pairs with a SessionEmitter (sdk/ts/src/control.ts:205-244, sdk/python/aep_sdk/control.py:116-140).

See also

On this page