API Reference
Conversational Threads API
Store a conversation as one ordered sequence of turns, then read its context back in a predictable order.
Threads are useful when order matters: chat transcripts, support handoffs, agent runs, and review histories. Aether assigns each turn a zero-based turn_index for the thread. Your app supplies a stable idempotency key so a lost response or retry cannot create a second turn.
Confirm availability before production use
Thread support rolls out across the hosted API and SDK packages separately. Confirm that your target environment serves /v1/threads, and install an SDK version that includes the Thread helper or raw thread methods, before depending on this surface in production.
SDK methods
| Operation | Python | TypeScript | .NET | Go |
|---|---|---|---|---|
| Append a turn | append_thread | appendThread | AppendThreadAsync | AppendThread |
| Read a thread | get_thread | getThread | GetThreadAsync | GetThread |
| Entity-scoped helper | memory.thread(...) | memory.thread(...) | memory.Thread(...) | memory.Thread(...) |
Every append returns a normal document record with two additional fields:
thread_id— the conversation you appended to.turn_index— the server-assigned position of that turn, starting at0.
Append a turn
Choose a thread ID that is meaningful in your application, such as a chat or ticket ID. Keep the idempotency key with the logical message you are writing. The SDK makes one automatically for retries within a call, but provide one yourself when a job may resume later.
turn = client.append_thread(
"support-8492",
"Customer reports that sign-in fails after resetting a password.",
tags=["support", "auth"],
metadata={"role": "user"},
idempotency_key="support-8492-message-17",
)
print(turn.thread_id, turn.turn_index) # support-8492 0
An append returns:
201 Createdfor a new turn.200 OKwhen the same idempotency key and same input are replayed.202 Acceptedwhen the turn is committed but its local content and search projection are still catching up. The canonical turn metadata is available throughget_threadimmediately. An authorized download on the designated origin returns retryable503 thread_projection_pendingwithRetry-Afteruntil projection finishes; the SDKs apply their configured bounded retry budget. If you need the projected content, retry the same append with the same idempotency key—or retry the content operation—instead of creating another turn. Unauthorized and wrong-scope reads remain indistinguishable404responses.
Reusing an idempotency key for any different append request or scope returns 409 Conflict. The comparison includes the tenant, thread ID, partition, text, metadata, tags, entity ID, source, reader list, and filename. Treat a conflict as a bug in the caller's job or retry bookkeeping, not a prompt to generate a new key blindly.
The server validates append size before reserving an index or charging the write. Text obeys the deployment's document-size limit. Metadata may serialize to at most 64 KiB; tags are limited to 128 entries, 1 KiB each, and 64 KiB serialized in aggregate; source and filename are limited to 1 KiB each; and the redacted canonical document payload is limited to 1 MiB. A violation returns 413 thread_append_too_large and does not consume the idempotency key, so a corrected request may reuse it.
Reader-list state is significant on append:
- Omit
acl_readersfor a turn visible to the whole tenant scope (and partition, when set). - Pass a non-empty list to limit reads to matching user or group principals, plus audited admin bypass.
- Pass an explicit empty list for admin-only quarantine.
The Memory.Thread.append helper does not accept a reader list and therefore creates tenant-visible turns. Use the raw append method when a conversation needs per-user access control, and keep the same reader policy across the thread when sequence membership is sensitive.
Read ordered context
get_thread returns records in chronological order by default. Use last_n_turns / lastNTurns (from 1 through 1000) when you only need recent context, and recent_first / recentFirst when your prompt builder prefers newest-first records. If you omit the window, the server still returns at most the newest 1000 readable turns; it never loads an unlimited transcript. Reader permissions are applied before the window, so a denied recent turn does not hide an older turn the caller may read.
turn_index is the canonical position and is not renumbered after reader filtering. A thread with different reader lists on different turns can therefore show gaps to a caller who cannot read every turn. Use one reader policy for the whole thread—or separate threads—when even the existence of a hidden turn is sensitive.
thread = client.get_thread("support-8492", last_n_turns=8)
for turn in thread.documents:
text = client.download_text(turn.doc_id)
print(turn.turn_index, text)
Thread reads return document metadata rather than an unbounded transcript. Fetch the text for the turns you intend to place in a prompt; this lets your application enforce its own token and privacy limits.
Thread IDs must contain 1 through 256 Unicode characters, cannot be whitespace-only, cannot contain control characters, and cannot be the reserved URL dot segments . or ... All four SDKs reject invalid IDs before making a request.
Turn bodies use the same encrypted document storage as ordinary Aether documents. The shared ordering record never stores plaintext passage text. An origin-only authenticated envelope retains the exact encrypted chunk and passage plan so projection does not depend on later chunking-setting changes; after the canonical append commits, the designated node projects that content into its local search indexes.
Build bounded prompt context
When a conversation belongs to the same entity you use with the Memory facade, create a Thread from that Memory. context combines the latest ordered turns with up to five semantic matches from the same entity and thread, de-duplicates them by document ID, and returns one flat list. The default ordered window is 10 turns; pass a value from 1 through 1000 to change it.
memory = Memory("customer-42", client=client)
thread = memory.thread("support-8492")
context = thread.context(
"What have we already tried for the sign-in problem?",
last_n_turns=8,
)
Recent turns come first in chronological order, followed by additional semantic matches. Set recent_first / recentFirst to reverse only the recent window. See Modeling conversations for complete support and therapy-oriented designs.
The helper downloads at most eight turns concurrently and applies one shared 16 MiB body-byte budget across recent turns and non-duplicate semantic matches. It stops scheduling later batches once that budget is crossed and returns the SDK's ordinary local error. This helper limit is separate from the raw get_thread cap of 1000 records.
The server selects the last N thread turns before the helper filters them to the Memory's entity. On a mixed-entity thread, context can therefore return fewer than N recent items and does not scan farther back to fill the window. Prefer one entity per helper-managed thread when you need a complete N-turn entity window; use the raw thread methods when a mixed-entity transcript is intentional.
Search within one thread
Use the thread filter with search or retrieve when you want semantic matching inside one conversation rather than the full knowledge base.
hits = client.retrieve("what did we promise about the reset link?", thread_id="support-8492")
The filter composes with entity IDs, tags, metadata, source, and a partition. A scoped client carries its partition boundary into thread reads, writes, and search just like other document operations.
Append-only lifecycle
Canonical turns cannot currently be changed through generic document update, delete, hard-delete, restore, re-embed, partition-move, entity-backfill, partition-delete, or snapshot-restore operations. Once a turn is projected on its origin node, document-by-ID mutations identify it as a turn and return 409 Conflict with code thread_turn_immutable. While a 202 Accepted append is awaiting projection, an authorized origin download returns 503 thread_projection_pending; retry within a bounded budget. Scope-wide operations such as entity backfill, partition deletion, and snapshot restore check the canonical thread records and reject an affected scope before changing it.
No thread deletion or retention API yet
There is no dedicated thread edit, deletion, or retention lifecycle today. The generic document hard-delete route does not delete canonical turns, and Memory.forget is not a workaround. If your application must erase transcripts on demand or by a fixed deadline—including regulated therapy records—do not store those transcripts as threads until a suitable lifecycle is available.
REST reference
| Method | Path | Notes |
|---|---|---|
POST | /v1/threads/{thread_id}/append?partition={partition} | JSON {text, metadata?, tags?, entity_id?, source?, acl_readers?, filename?} plus a required Idempotency-Key header of 1-256 ASCII characters. The optional partition query parameter is part of the append's durable scope. |
GET | /v1/threads/{thread_id} | Optional last_n_turns (1-1000), recent_first, and partition query parameters. |
GET | /v1/search?...&thread_id={thread_id} | Semantic search restricted to one thread. |
Direct REST calls use Authorization: Bearer <api-key>. The SDKs add this header and use the versioned /v1 paths automatically.