memories.create to write and fetch to read. That is complete and correct on its own.
Real-time anticipation adds a second channel. instance.listen() opens a long-lived gRPC stream. Your app reports what the agent is doing; Synap predicts what it will need next and pushes context bundles down the stream before the agent asks. The next fetch() then resolves from a local cache instead of a network round-trip.
Two channels, two jobs
The two channels do different work, and the difference is mostly about when memory happens.Opening the stream
listen() requires an initialized SDK. It resolves your client_id and instance_id, authenticates the stream with the same API key your REST calls use, and holds the connection open until you close it.
Both callbacks take one argument —
on_reconnect receives the attempt count, on_disconnect receives the reason. A zero-argument callback raises TypeError at the moment the stream drops, which is precisely when you need the diagnostic.on_context is optional. Arriving bundles are written to the SDK’s anticipation cache automatically, so fetch() finds them whether or not you supply a callback. Use it only when you want to react to bundles directly.
Connection targets are configurable via SDKConfig: grpc_host, grpc_port, and grpc_use_tls. Defaults point at Synap Cloud with TLS on.
What you send
Report agent activity withsend_message. The event_type determines how the platform treats it.
What the stream does to memory
Auser_message or assistant_message event does more than feed anticipation. The platform writes it to the conversation’s history, and that history has a second consumer.
Once a conversation crosses its compaction threshold — by default 3,000 tokens or 10 messages, configurable per Instance — Synap compacts it. Compaction summarizes the conversation for short-term context, and then promotes the raw turns into the long-term ingestion pipeline: the same extraction stages memories.create() runs, producing memories of the same quality.
So turns sent over the stream do become durable memory. Four things decide whether that actually happens for a given conversation:
required
Below the threshold, nothing is promoted. Short conversations — a two-turn support exchange, a one-shot question — never produce memories this way.
required
A turn missing
user_id or customer_id is never persisted in the first place, so there is nothing to promote. It fails silently.required
Promotion derives its scope from the conversation record. If that cannot be resolved, promotion is skipped.
note
If you ingested the conversation with
conversation.ingest_transcript, promotion skips it deliberately — the full transcript was already ingested at higher fidelity, and extracting again would duplicate it.What the SDK sends on its own
Once the stream is live, the SDK instrumentsfetch() for you. You do not write this code, but you should know it exists:
context_fetch— a retrieval was requestedcontext_used— the retrieval was served from the anticipation cachecontext_assembled— what the SDK actually composed for the model
What comes back
The platform pushes context bundles down the stream. Each is written into the SDK’s in-process anticipation cache. When your agent then callsfetch():
- Cache hit → served locally in roughly a millisecond, no network call
- Cache miss → falls through to the normal REST retrieval path
The turn loop
The mechanism that makes this work is easy to miss: theassistant_message you send at the end of turn N is what warms the cache for turn N+1. Anticipation happens between turns, not during them. By the time your next turn calls fetch(), the bundle has already landed.
So the ordering matters. Emit assistant_message after your agent replies — not before.
This example ingests every turn and streams every turn, which is the double-extraction case described above. It is written this way to show both calls in one place. In production, pick one owner for durable memory.
One stream per instance
In a multi-tenant server, open one stream for the whole process and distinguish users through theuser_id / customer_id on each send_message and fetch. Do not open a stream per user session.
The platform enforces per-instance and per-client stream quotas (currently 100 concurrent streams per instance, 200 per client). A stream-per-session design saturates that quota under real concurrency, and the surplus connections enter a RESOURCE_EXHAUSTED reconnect loop.
Streams also have a maximum lifetime of one hour, after which the server closes them. This is normal: the SDK reconnects with exponential backoff and your
on_reconnect callback fires. A long-lived server should expect periodic reconnects, not a single permanent connection.When the stream fails
Failure is designed to be non-fatal — retrieval falls back to REST — but the two SDKs differ in how loudly they say so.What anticipation does not do
- It does not ingest a turn at the moment you send it — promotion happens at compaction, or not at all if the conversation stays short
- It does not give you control over how promoted content is typed, scoped, or tagged;
memories.create()does - It does not replace
memories.createorconversation.ingest_transcriptas your memory strategy - It does not change what
fetch()returns — only how fast it returns - It is not required; every Synap feature works without it
Run it in a server
One shared stream, many tenants, and the failure modes worth alerting on.
The streaming API
listen, send_message, and stop_listening in full.