Skip to content

Aether Transport (Python SDK)

The MemoryLayer Python SDK can be configured to talk to the server over Aether instead of plain HTTP. Aether is an optional service-mesh transport that brings mTLS, signed identity headers, on-behalf-of (OBO) delegation, and cross-datacenter service discovery. For background on Aether itself, see aetherlayer.ai; for how the MemoryLayer server side is wired up, see Server -> Aether Transport.

This page describes the client-side surface in memorylayer-client.


When to Use This

Switch the SDK to Aether transport when:

  • The MemoryLayer server is reachable through your Aether mesh (registered as sv::memorylayer::default)
  • You want mTLS rather than terminating HTTPS yourself
  • You need to call MemoryLayer on behalf of an end user with an audit trail (OBO)
  • You are deploying across datacenters and don’t want to expose the MemoryLayer HTTP port directly

For local development or single-process deployments, the default HTTP transport remains the right choice.


Quick Start

import asyncio
from memorylayer import MemoryLayerClient
from scitrera_aether_client import AsyncAgentClient # provided by Aether
async def main():
aether_client = AsyncAgentClient(...) # built per your Aether install
await aether_client.connect()
async with MemoryLayerClient(
transport="aether",
aether_client=aether_client,
aether_target="sv::memorylayer::default",
workspace_id="my-workspace",
) as client:
await client.remember("MemoryLayer reached via Aether")
results = await client.recall("Aether")
for m in results.memories:
print(m.content)
await aether_client.aclose()
asyncio.run(main())

Two things change relative to the HTTP example:

  1. The client is constructed with transport="aether" and an already-connected aether_client plus a aether_target topic.
  2. The SDK does not own the aether_client connection. MemoryLayerClient.aclose() is a no-op for the connection; you construct and dispose of the aether_client yourself.

Constructor Parameters

ParameterDefaultDescription
transport"http""http" for direct httpx calls (uses base_url), "aether" to route through Aether, or a custom Transport instance.
aether_clientNoneRequired when transport="aether". An already-connected AsyncAgentClient (or compatible) from scitrera_aether_client.
aether_target"sv::memorylayer::default"Service topic to address.
default_authorityNoneOptional AuthorityContext applied to every request (see OBO below).
api_keyNoneSame as HTTP — passed through as Authorization: Bearer for the terminator to forward.
session_idNoneSent as X-Session-ID.
workspace_idNoneSent as X-Workspace-ID.
timeout30.0Per-request timeout.
base_path"/v1"API prefix used when constructing the inner request path.

When transport="aether" is set and aether_client is None, the constructor raises ValueError immediately. This is intentional: the SDK does not attempt to import or initialize the Aether client library on your behalf.


On-Behalf-Of (OBO) Delegation

Aether’s strongest feature for multi-user applications is OBO: your application service can call MemoryLayer as a specific end user, with the mesh signing the delegation. The SDK exposes this through an acting_for(...) context manager.

Single-request OBO

from memorylayer.models import AuthorityContext, PrincipalRef
authority = AuthorityContext(
grant_id="grant_abc123", # issued by your auth layer
subject=PrincipalRef(type="User", id="alice@example.com"),
)
with client.acting_for(authority):
# Every request inside this block is on Alice's behalf
await client.remember("Alice's preference: dark mode")
results = await client.recall("preferences")

Under the hood the SDK injects four headers:

HeaderValue
X-Aether-Grant-IDThe grant token id
X-Aether-Authority-ModeAuthority mode (on_behalf_of by default)
X-Aether-Subject-TypeSubject principal type
X-Aether-Subject-IDSubject principal id

When routed over Aether transport, these headers are translated into the canonical AuthorizationContext proto field on the ProxyHttpRequest, which the server’s Aether terminator then verifies and mints into X-Auth-* headers for FastAPI. Header-based propagation is intentionally avoided — strict-mode terminators strip inbound X-Aether-* / X-Auth-* headers before minting anyway, so the proto field is the reliable channel.

Default authority

If every request from a client instance should run as the same principal, pass default_authority= to the constructor instead of using acting_for:

client = MemoryLayerClient(
transport="aether",
aether_client=aether_client,
default_authority=authority,
)

The context-manager form takes precedence: acting_for(...) temporarily overrides default_authority for the duration of the block.


Connection Ownership

The AetherTransport does not own the underlying aether_client. Concretely:

  • MemoryLayerClient.aclose() and MemoryLayerClient.__aexit__ close the SDK’s transport wrapper, but they do not disconnect the aether_client.
  • You are responsible for await aether_client.aclose() at process shutdown.

This matches the pattern used by other Aether-aware Python services (cowork, embed-server’s internal embed client, etc.) — one shared, long-lived Aether connection per process, fan-out across many client wrappers.


Streaming & Uploads

All current SDK methods work identically over HTTP and Aether transport, including:

  • NDJSON streaming — export_workspace_stream(...) and import_workspace_stream(...) yield/accept parsed line objects and run through the shared Transport.request(...) surface. The public API and the yielded shape are unchanged.
  • Multipart uploads — document ingestion serializes the multipart body to bytes transport-agnostically (_encode_multipart) and ships the raw bytes + caller-provided content-type through whichever transport is active.

Underlying chunked-bytes support

Aether itself supports incremental byte-streaming end-to-end. The underlying scitrera_aether_client.proxy module accepts stream_response=True (and stream_idle_timeout_ms) on its proxy call and returns an _ProxyStreamAsync iterator that yields bytes chunks as ProxyHttpBodyChunk frames arrive, terminated by a fin flag (or a mid-stream ProxyError). So the mesh does not buffer the whole response on either side.

What the MemoryLayer SDK’s current AetherTransport.request(...) does is call proxy_http_async(...) without stream_response=True, which means today the SDK consumes the body as a single ProxyHttpResponse. That is a SDK-side implementation choice, not an Aether limitation. The two currently-shipped streaming methods buffer-then-split anyway, so no behaviour is lost over Aether transport.

If a future SDK method needs true aiter_bytes-style chunk-by-chunk consumption (e.g. tailing a server-sent event endpoint, downloading a huge export without buffering), AetherTransport can be extended with a sibling astream(...) entry point that sets stream_response=True and returns the chunk iterator — no protocol or terminator changes required. Until that lands, the _ensure_client() escape hatch on the client is the way to get the raw httpx.Response with aiter_bytes when running on the HTTP transport.


Error Handling

The same exception hierarchy applies regardless of transport: MemoryLayerError, AuthenticationError, NotFoundError, ValidationError, RateLimitError, ServerError. The Aether transport maps ProxyHttpResponse status codes through the same mapping the HTTP transport uses.

If the Aether call itself fails (e.g. mesh routing error, target topic unreachable), the SDK wraps the error in MemoryLayerError with a descriptive message. The underlying Aether exception is preserved as the cause and can be retrieved via error.__cause__.