GoodMem
ReferenceClient SDKs

Java SDK

The GoodMem Java SDK offers an OpenAI-style API where any operation is accessed through client.<namespace>.<method>(...) on a Goodmem instance.

Installation

Available on Maven Central as ai.pairsys:goodmem-java.

// build.gradle.kts
repositories { mavenCentral() }

dependencies {
    implementation("ai.pairsys:goodmem-java:0.1.2")
}
<!-- pom.xml -->
<dependency>
    <groupId>ai.pairsys</groupId>
    <artifactId>goodmem-java</artifactId>
    <version>0.1.2</version>
</dependency>

To build from source instead (e.g., to depend on an unreleased commit):

cd goodmem/clients/v2/java
./gradlew publishToMavenLocal     # installs to ~/.m2/repository/

Then add mavenLocal() to your repositories block alongside mavenCentral().

Requirements: JDK 21 LTS. Brings in OkHttp 4 (HTTP) and Jackson 2.18 (JSON) transitively.

Clients

The Java SDK ships two clients with identical construction surfaces and parallel methods:

  • Goodmem — synchronous. Every method returns the response directly or throws.
  • AsyncGoodmem — asynchronous. Every method returns CompletableFuture<T>.

Most of the usage details in this page apply equally to both; code samples default to the sync client. For the async surface see Async client below.

Construction

Goodmem is constructed via its Builder in two mutually exclusive patterns.

Pattern 1 — Simple mode (baseUrl + apiKey):

import ai.pairsys.goodmem.client.Goodmem;

Goodmem client = Goodmem.builder()
    .baseUrl("http://localhost:8080")
    .apiKey("gm_...")
    .timeout(java.time.Duration.ofSeconds(60))  // optional; defaults to 30s
    .build();
  • baseUrl — Goodmem server URL, e.g., "http://localhost:8080" (no v1 suffix)
  • apiKey — Goodmem API key, e.g., "gm_..."
  • timeout — Call/connect/read/write timeout. Defaults to 30 seconds. Increase for long operations like RAG retrieval with LLM generation.

Pattern 2 — http_client mode (custom OkHttpClient):

import okhttp3.OkHttpClient;

Goodmem client = Goodmem.builder()
    .baseUrl("http://localhost:8080")
    .apiKey("gm_...")
    .httpClient(myOkHttpClient)  // fully configured by you
    .build();

Use this when you need interceptors (logging, tracing, retry), a proxy, a custom TLS trust store, or connection pool tuning. You own the client's lifecycle — goodmem.close() is a no-op under this mode.

try-with-resources

Goodmem implements AutoCloseable. Use it as a resource so the connection pool is released deterministically:

try (Goodmem client = Goodmem.builder()
        .baseUrl("http://localhost:8080")
        .apiKey("gm_...")
        .build()) {
    // use client
}

Quickstart

import ai.pairsys.goodmem.client.Goodmem;
import ai.pairsys.goodmem.client.models.*;

try (Goodmem client = Goodmem.builder()
        .baseUrl("http://localhost:8080")
        .apiKey("gm_...")
        .build()) {

    // Create an embedder. Registry auto-fills provider, endpoint,
    // dimensionality, max_sequence_length, distribution_type from the
    // model_identifier; apiKey is converted to structured credentials.
    EmbedderResponse embedder = client.embedders.create(
        EmbedderCreationRequest.builder()
            .displayName("My OpenAI")
            .modelIdentifier("text-embedding-3-large")
            .build(),
        "sk-..."
    );

    // Create a space. DEFAULT_CHUNKING_CONFIG is auto-injected when unset.
    Space space = client.spaces.create(
        SpaceCreationRequest.builder()
            .name("My Space")
            .label("env", "dev")
            .spaceEmbedders(java.util.List.of(new SpaceEmbedderConfig(embedder.embedderId(), null)))
            .publicRead(false)
            .build());

    // Pagination is automatic — iterate across all pages.
    for (Memory m : client.memories.list(space.spaceId())) {
        System.out.println(m.memoryId());
    }
}

TLS configuration

Four situations cover the typical needs:

1. Default — trusts system CAs

No configuration needed. Works for any server with a cert from a public CA.

2. Skip verification (localhost / dev)

Build an OkHttpClient that trusts all certificates, then pass it in http_client mode. Never use in production — an attacker in the middle can impersonate the server:

import javax.net.ssl.*;
import java.security.cert.X509Certificate;

TrustManager[] trustAll = new TrustManager[] {
    new X509TrustManager() {
        public void checkClientTrusted(X509Certificate[] c, String a) {}
        public void checkServerTrusted(X509Certificate[] c, String a) {}
        public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
    }
};
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, trustAll, new java.security.SecureRandom());

OkHttpClient http = new OkHttpClient.Builder()
    .sslSocketFactory(ctx.getSocketFactory(), (X509TrustManager) trustAll[0])
    .hostnameVerifier((host, session) -> true)
    .build();

Goodmem client = Goodmem.builder()
    .baseUrl("https://localhost:8081")
    .apiKey("gm_...")
    .httpClient(http)
    .build();

3. Custom CA — trusts only that CA

Load your CA into a KeyStore and build a TrustManagerFactory:

import java.security.KeyStore;
import java.security.cert.CertificateFactory;
import javax.net.ssl.*;

KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null, null);
try (var in = new java.io.FileInputStream("/path/to/rootCA.pem")) {
    ks.setCertificateEntry("ca", CertificateFactory.getInstance("X.509").generateCertificate(in));
}

TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ks);
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, tmf.getTrustManagers(), null);

OkHttpClient http = new OkHttpClient.Builder()
    .sslSocketFactory(ctx.getSocketFactory(), (X509TrustManager) tmf.getTrustManagers()[0])
    .build();

4. Custom CA + system CAs

If you need to trust both your CA and the default system trust store, load both into a KeyStore:

KeyStore defaultKs = KeyStore.getInstance(KeyStore.getDefaultType());
defaultKs.load(null);
TrustManagerFactory defaultTmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
defaultTmf.init((KeyStore) null);  // loads system CAs

KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null, null);
// add default CAs
for (TrustManager tm : defaultTmf.getTrustManagers()) {
    if (tm instanceof X509TrustManager xtm) {
        int i = 0;
        for (X509Certificate c : xtm.getAcceptedIssuers()) ks.setCertificateEntry("sys-" + i++, c);
    }
}
// add your CA
try (var in = new java.io.FileInputStream("/path/to/rootCA.pem")) {
    ks.setCertificateEntry("mine", CertificateFactory.getInstance("X.509").generateCertificate(in));
}
// then build TrustManagerFactory + OkHttpClient as in #3

Proxy

OkHttpClient http = new OkHttpClient.Builder()
    .proxy(new java.net.Proxy(Proxy.Type.HTTP, new java.net.InetSocketAddress("proxy.example", 8080)))
    .build();

Request logging

import okhttp3.logging.HttpLoggingInterceptor;

HttpLoggingInterceptor log = new HttpLoggingInterceptor();
log.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient http = new OkHttpClient.Builder().addInterceptor(log).build();

Requires the extra dep: com.squareup.okhttp3:logging-interceptor.

Methods

Each namespace below links to its full class Javadoc on javadoc.io — that's where the per-method @param contract, @throws block, Example code, and REST equivalent all live. The MDX page below this section is the narrative reference; the Javadoc is the per-method reference.

admin

Server lifecycle ops — drain, license reload, background-job purge.

Javadoc: ai.pairsys.goodmem.client.api.AdminAPI

MethodDescription
backgroundJobsPurgePurge completed background jobs
drainRequest the server to enter drain mode
licenseReloadReload the active license from disk
retrieveMemoryLogPoliciesCreateCreate a RetrieveMemory log policy
retrieveMemoryLogPoliciesDeleteDelete a RetrieveMemory log policy
retrieveMemoryLogPoliciesGetGet a RetrieveMemory log policy
retrieveMemoryLogPoliciesListList RetrieveMemory log policies

apikeys

API key lifecycle — create, list, update, soft-delete.

Javadoc: ai.pairsys.goodmem.client.api.ApikeysAPI

MethodDescription
createCreate a new API key
deleteDelete an API key Permanently deletes an API key
listList API keys
updateUpdate an API key

embedders

Embedder management — provider configuration + lifecycle.

Javadoc: ai.pairsys.goodmem.client.api.EmbeddersAPI

MethodDescription
createCreate a new embedder
deleteDelete an embedder
getGet an embedder by ID
listList embedders
updateUpdate an embedder

llms

LLM management — generation-time model registration.

Javadoc: ai.pairsys.goodmem.client.api.LlmsAPI

MethodDescription
createCreate a new LLM
deleteDelete an LLM
getGet an LLM by ID
listList LLMs
updateUpdate an LLM

memories

Memory CRUD + retrieval (streaming) + file upload + batch ops.

Javadoc: ai.pairsys.goodmem.client.api.MemoriesAPI

MethodDescription
batchCreateCreate multiple memories in a single batch
batchDeleteDelete memories in batch
batchGetGet multiple memories by ID
contentDownload memory content
createCreate a memory from text content or base64-encoded binary content
deleteDelete a memory
getGet a memory by ID
listLists memories within a given space
pagesList memory page images
pagesImageDownload memory page image content
retrievePerforms a streaming semantic search across one or more memory spaces and returns matching chunks ranked by relevance as well as…

ocr

Document OCR — text extraction from PDFs / images.

Javadoc: ai.pairsys.goodmem.client.api.OcrAPI

MethodDescription
documentRun OCR on a document or image

ping

Endpoint health probes — single-shot and streaming.

Javadoc: ai.pairsys.goodmem.client.api.PingAPI

MethodDescription
onceRun a single ping probe
streamStream ping probe results

rerankers

Reranker management — re-scoring of retrieval hits.

Javadoc: ai.pairsys.goodmem.client.api.RerankersAPI

MethodDescription
createCreate a new reranker
deleteDelete a reranker
getGet a reranker by ID
listList rerankers
updateUpdate a reranker

spaces

Memory space management — the top-level container for memories.

Javadoc: ai.pairsys.goodmem.client.api.SpacesAPI

MethodDescription
createCreate a new Space
deleteDelete a space
getGet a space by ID
listList spaces accessible to the caller, with optional filtering by owner, labels, and name
updateUpdate a space

system

Server info + system initialization.

Javadoc: ai.pairsys.goodmem.client.api.SystemAPI

MethodDescription
infoRetrieve server build metadata
initInitialize the system

users

User lookup by id, email, or me.

Javadoc: ai.pairsys.goodmem.client.api.UsersAPI

MethodDescription
getRetrieves a user by ID or email address
meGet current user profile

Request builders

Every request record (EmbedderCreationRequest, SpaceCreationRequest, JsonMemoryCreationRequest, RetrieveMemoryRequest, RerankerCreationRequest, LLMCreationRequest, etc.) exposes a nested Builder so you can set only the fields you care about and skip the long positional null, null, null, … constructor:

EmbedderCreationRequest req = EmbedderCreationRequest.builder()
    .displayName("My OpenAI")
    .modelIdentifier("text-embedding-3-large")
    .build();

Map<String, String> fields named labels, hints, or metadata get a singular-form convenience setter that appends to the map:

SpaceCreationRequest req = SpaceCreationRequest.builder()
    .name("My Space")
    .label("env", "dev")       // appends to labels
    .label("team", "search")
    .spaceEmbedders(List.of(new SpaceEmbedderConfig(embedderId, null)))
    .build();

Under the hood the Builder just calls the record's positional constructor. Null unset fields are omitted from the wire payload thanks to @JsonInclude(NON_NULL). You can still use the positional constructor directly if you prefer — the Builder is purely additive.

Typed list/get options

List and get methods expose a typed XxxListOptions / XxxGetOptions record so you don't have to remember wire-format keys (maxResults, sortOrder, …) and can let the IDE autocomplete them:

Page<Space> page = client.spaces.list(
    SpaceListOptions.builder()
        .maxResults(50)
        .nameFilter("research*")
        .sortOrder(SortOrder.ASCENDING)
        .label("env", "prod")       // flattened to label.env=prod on the wire
        .build());

Null-valued fields are dropped from the query string. Map<String, String> fields named label are flattened into label.<key>=<value> entries to match the server's label-filter syntax. Passing null for the options argument is equivalent to an empty filter.

Eight options classes are generated:

Options typeMethod
SpaceListOptionsspaces.list(options)
EmbedderListOptionsembedders.list(options)
LLMListOptionsllms.list(options)
RerankerListOptionsrerankers.list(options)
MemoryListOptionsmemories.list(spaceId, options)
MemoryGetOptionsmemories.get(id, options)
MemoryPageListOptionsmemories.pages(id, options)
MemoryPageImageOptionsmemories.pagesImage(id, pageIndex, options)

The original Map<String, Object> overload is still available as an escape hatch — it's renamed to listRaw(...) / getRaw(...) / pagesRaw(...) / pagesImageRaw(...) so the primary method name belongs to the typed path:

// Typed (preferred):
client.spaces.list(SpaceListOptions.builder().maxResults(50).build());

// Raw escape hatch:
client.spaces.listRaw(Map.of("maxResults", 50));

UUID-typed path parameters

Every get / delete / update method that takes a resource id offers a java.util.UUID overload alongside the String form. The generator converts via .toString() internally:

UUID spaceUuid = UUID.fromString("...");
Space typed = client.spaces.get(spaceUuid);  // UUID-typed
Space raw   = client.spaces.get("sp_...");   // String-typed (escape hatch)

memories.list(spaceId, options) and memories.pagesImage(id, pageIndex, ...) also get UUID + long pageIndex overloads, plus a combined (UUID, long, ...) variant so the strongly-typed signatures compose without forcing you to mix.

Factory methods for oneOf models

Records with an "exactly one of" contract (ChunkingConfiguration, ContextItem, PingEvent, RetrievedItem) expose static factory methods per variant, so you can't accidentally hand-null sibling fields:

ChunkingConfiguration cfg = ChunkingConfiguration.recursive(
    RecursiveChunkingConfiguration.builder()
        .chunkSize(512)
        .chunkOverlap(64)
        .build());

RetrievedItem item = RetrievedItem.memory(memory);

Encoded-primitive convenience setters

The Builder for each request record overloads a handful of high-churn fields with stronger Java types:

WhereString/Long baseTyped overloadWhat it does
endpointUrl / apiPath / monitoringEndpointStringjava.net.URI.toString()
*Ms timeout fieldsLongjava.time.Duration.toMillis()
*Sec timeout fieldsLongjava.time.Duration.toSeconds()
*At timestamp fields (e.g. expiresAt)Longjava.time.Instant.toEpochMilli()
JsonMemoryCreationRequest.originalContentStringbyte[]base64-encodes into originalContentB64
RetrieveMemoryRequest.postProcessorPostProcessorChatPostProcessorConfig.toPostProcessor()
CreateApiKeyRequest req = CreateApiKeyRequest.builder()
    .label("scope", "ci")
    .expiresAt(Instant.now().plus(Duration.ofDays(30)))
    .build();

Use ai.pairsys.goodmem.client.MediaTypes for common contentType values: TEXT_PLAIN, APPLICATION_PDF, IMAGE_PNG, etc. — avoids string-literal typos.

Typed configs for the built-in RAG chat processor

ai.pairsys.goodmem.client.ChatPostProcessorConfig is a fluent, validated builder for the ChatPostProcessor factory — the common RAG pipeline. Numeric fields enforce documented bounds where applicable (llmTemp [0, 2], positive token budgets, positive maxResults):

RetrieveMemoryRequest req = RetrieveMemoryRequest.builder()
    .message("What do you know about AI?")
    .spaceKeys(List.of(new SpaceKey(spaceId, null, null)))
    .postProcessor(ChatPostProcessorConfig.builder()
        .llmId(llmId)
        .genTokenBudget(2048L)
        .llmTemp(0.2)
        .build())
    .build();

For non-built-in processors, keep using new PostProcessor(factoryName, configMap) directly.

Typed status enums

Four IR-declared status fields now deserialize as typed Java enums instead of raw strings — so pattern matching and switch on known states works:

  • Memory.processingStatusMemoryProcessingStatus
  • ApiKeyResponse.status / UpdateApiKeyRequest.statusApiKeyStatus
  • BackgroundJobSummary.statusBackgroundJobStatus
  • AdminDrainResponse.stateAdminDrainState

Unknown server values fail Jackson deserialization loudly; if the server adds a new state, regenerate from an updated IR.

Range validation on Builder setters

Setters on documented numeric bounds throw IllegalArgumentException at call time rather than waiting for the server to reject. Null still clears the field. Covered today:

FieldBound
dimensionality, maxSequenceLength, chunkSize, maxResults, limit, count, efSearch, maxScan, maxInFlight≥ 1
chunkOverlap≥ 0
temperature[0.0, 2.0]
topP[0.0, 1.0]
frequencyPenalty, presencePenalty[-2.0, 2.0]

Common Data Models

Types shared across multiple API namespaces. Models are Java records with Jackson annotations; JSON wire names are camelCase (matching the record component names).

Page<T>

ai.pairsys.goodmem.client.Page<T> — a page of results from a list endpoint. Returned by every method that auto-paginates (spaces.list, memories.list). Iterates across all pages lazily; subsequent pages are fetched on demand. Page<T> does not implement AutoCloseable — the underlying HTTP connection is released per call, so there's nothing to close.

Page<Space> page = client.spaces.list(SpaceListOptions.builder().maxResults(50).build());

// First-page items only
List<Space> first = page.items();

// Auto-paginate through everything (lazy)
for (Space s : page) { process(s); }

// Manual page advance
while (page.hasMore()) {
    page = page.next();
    for (Space s : page.items()) { process(s); }
}
  • items()List<T> — items in this page
  • nextToken()String — continuation token, or null if exhausted
  • hasMore()boolean — true iff nextToken != null
  • next()Page<T> — fetch next page; throws NoSuchElementException when exhausted
  • iterator()Iterator<T> — lazy cross-page iteration; can only be called once

RetrieveMemoryStream

ai.pairsys.goodmem.client.RetrieveMemoryStream — iterable, closeable NDJSON stream of RetrieveMemoryEvent returned by memories.retrieve. Each newline-delimited JSON document is parsed into one event. Always use try-with-resources to release the underlying HTTP connection:

RetrieveMemoryRequest req = RetrieveMemoryRequest.builder()
    .message("question")
    .spaceId(spaceId)
    .build();
try (RetrieveMemoryStream events = client.memories.retrieve(req)) {
    for (RetrieveMemoryEvent evt : events) {
        process(evt);
    }
}

The iterator can only be consumed once — NDJSON cannot be rewound. To restart, call memories.retrieve again.

Defaults

ai.pairsys.goodmem.client.Defaults — hand-written SDK default constants. Currently exposes:

  • DEFAULT_CHUNKING_CONFIG (ChunkingConfiguration) — recursive chunking at 512 characters with 64-character overlap, KEEP_END boundary strategy, measured by characters. Auto-injected by spaces.create when defaultChunkingConfig is null. Reference it directly if you want to start from the defaults and tweak:

    import ai.pairsys.goodmem.client.Defaults;
    ChunkingConfiguration base = Defaults.DEFAULT_CHUNKING_CONFIG;

ApiKeyAuth

Configuration for classic API-key authentication.

  • inlineSecret (String, optional) — secret stored directly in GoodMem (mutually exclusive with secretRef)
  • secretRef (SecretReference, optional) — reference to an external secret manager entry
  • headerName (String, optional) — HTTP header to carry the credential (defaults to Authorization)
  • prefix (String, optional) — prepended to the secret (e.g., "Bearer ")

ChunkingConfiguration

Chunking strategy used when processing content. Exactly one of none, recursive, or sentence must be set.

CredentialKind

Java enum with @JsonProperty wire values: CREDENTIAL_KIND_UNSPECIFIED, CREDENTIAL_KIND_API_KEY, CREDENTIAL_KIND_GCP_ADC.

DistributionType

Java enum: DENSE, SPARSE.

EndpointAuthentication

Structured credential payload describing how GoodMem authenticates with an upstream provider.

  • kind (CredentialKind) — selected credential strategy
  • apiKey (ApiKeyAuth, optional) — set when kind == CREDENTIAL_KIND_API_KEY
  • gcpAdc (GcpAdcAuth, optional) — set when kind == CREDENTIAL_KIND_GCP_ADC
  • labels (Map<String, String>, optional) — operator-facing annotations

GcpAdcAuth

Configuration for Google Application Default Credentials.

  • scopes (List<String>, optional) — additional OAuth scopes
  • quotaProjectId (String, optional) — quota project for billing

GoodMemStatus

Non-fatal warning or status (operation continues).

  • code (String) — machine-readable code
  • message (String) — human-readable message
  • details (Map<String, String>, optional) — additional context

LengthMeasurement

Java enum: CHARACTER_COUNT, TOKEN_COUNT, CUSTOM.

LLMProviderType

Java enum for LLM providers: OPENAI, LITELLM_PROXY, OPEN_ROUTER, VLLM, OLLAMA, LLAMA_CPP, CUSTOM_OPENAI_COMPATIBLE.

Modality

Java enum: TEXT, IMAGE, AUDIO, VIDEO.

NoChunkingConfiguration

Empty record — used as the none variant of ChunkingConfiguration to preserve content as a single unit.

ProviderType

Java enum for embedder/reranker providers: OPENAI, VLLM, TEI, LLAMA_CPP, VOYAGE, COHERE, JINA.

RecursiveChunkingConfiguration

Recursive hierarchical chunking with configurable separators and overlap.

  • chunkSize (Long) — maximum size of a chunk
  • chunkOverlap (Long) — sliding overlap between adjacent chunks
  • separators (List<String>, optional) — hierarchy of splitting separators
  • keepStrategy (SeparatorKeepStrategy) — how to handle separator characters after splitting
  • separatorIsRegex (Boolean, optional) — treat separators as regex
  • lengthMeasurement (LengthMeasurement) — unit for chunkSize/chunkOverlap

SecretReference

Reference to an external secret manager entry.

  • uri (String) — identifier for the resolver (vault://, env://, …)
  • hints (Map<String, String>, optional) — metadata for the resolver (e.g., {"encoding":"base64"})

SentenceChunkingConfiguration

Sentence-based chunking with language detection.

  • maxChunkSize (Long) — maximum chunk size
  • minChunkSize (Long) — minimum before creating a new chunk
  • enableLanguageDetection (Boolean, optional) — detect language for better segmentation
  • lengthMeasurement (LengthMeasurement) — unit for size fields

SeparatorKeepStrategy

Java enum: KEEP_NONE, KEEP_START, KEEP_END.

SortOrder

Java enum: ASCENDING, DESCENDING, SORT_ORDER_UNSPECIFIED.

Errors

All SDK methods throw typed unchecked exceptions on HTTP errors. The hierarchy is rooted at GoodmemException, with ApiException for any HTTP 4xx/5xx and per-status subclasses for the common ones. Error class names align with the Python SDK's naming.

ExceptionHTTPDescription
GoodmemExceptionBase exception for all SDK errors (also wraps I/O failures)
ApiExceptionany 4xx/5xxGeneric HTTP error (has getStatusCode(), getBody())
BadRequestException400Malformed or invalid request
AuthenticationException401Invalid or missing API key
PermissionDeniedException403Insufficient permissions
NotFoundException404Resource not found
ConflictException409Conflict (e.g., duplicate id)
UnprocessableEntityException422Invalid request parameters
RateLimitException429Too many requests
InternalServerException5xxServer-side error
import ai.pairsys.goodmem.client.errors.*;

try {
    Memory memory = client.memories.get("nonexistent-id");
} catch (NotFoundException e) {
    System.out.println("Memory not found");
} catch (ApiException e) {
    System.out.println("API error " + e.getStatusCode() + ": " + e.getBody());
}

All exceptions are RuntimeExceptions — no checked-exception surface.

File upload convenience

memories.create(String spaceId, Path filePath) reads a file from disk, base64-encodes it, infers a contentType via Files.probeContentType, and posts a JSON memory:

import java.nio.file.Path;

Memory memory = client.memories.create(
    space.spaceId(),
    Path.of("/tmp/report.pdf"));

For large files consider streaming the bytes yourself or using an async upload in your own code — the built-in overload reads the file fully into memory before base64-encoding.

Javadoc

Every class, method, field, and parameter is documented in full Javadoc. Locally:

cd goodmem/clients/v2/java
./gradlew javadoc
open build/docs/javadoc/index.html

javadoc.io hosts the per-class API reference automatically once releases land on Maven Central: https://javadoc.io/doc/ai.pairsys/goodmem-java/latest/.

Async client

AsyncGoodmem is a parallel client where every method returns CompletableFuture<T> instead of blocking for the response. Construction mirrors Goodmem exactly — same Builder, same two modes (simple / custom OkHttpClient), same timeout / close semantics.

import ai.pairsys.goodmem.client.AsyncGoodmem;
import ai.pairsys.goodmem.client.models.SystemInfoResponse;

try (AsyncGoodmem client = AsyncGoodmem.builder()
        .baseUrl("http://localhost:8080")
        .apiKey("gm_...")
        .build()) {

    // Single call — `.get()` or `.join()` if you want to block here
    SystemInfoResponse info = client.system.info().get();

    // Composition — ideal for pipelines
    EmbedderCreationRequest req = EmbedderCreationRequest.builder()
        .displayName("My OpenAI")
        .modelIdentifier("text-embedding-3-large")
        .build();
    client.embedders
        .create(req, "sk-...")
        .thenCompose(emb -> client.spaces.create(
            SpaceCreationRequest.builder()
                .name("My Space")
                .spaceEmbedders(java.util.List.of(
                    new SpaceEmbedderConfig(emb.embedderId(), null)))
                .build()))
        .thenAccept(space -> System.out.println("Space ready: " + space.spaceId()))
        .exceptionally(ex -> { ex.printStackTrace(); return null; })
        .join();
}

Under the hood, the async client uses OkHttp's native async callbacks — no thread pool is required beyond OkHttp's own dispatcher, and the same connection pool is shared across concurrent requests. If you need more concurrency control (custom dispatcher executor, rate limiting, etc.), build an OkHttpClient yourself and pass it via .httpClient(...).

Async pagination

List endpoints return CompletableFuture<AsyncPage<T>> instead of Page<T>. AsyncPage<T> exposes the current page's items synchronously (they're already loaded); .next() returns CompletableFuture<AsyncPage<T>> to fetch the next page.

static CompletableFuture<List<Space>> collectAll(AsyncPage<Space> page, List<Space> acc) {
    acc.addAll(page.items());
    return page.hasMore()
        ? page.next().thenCompose(p -> collectAll(p, acc))
        : CompletableFuture.completedFuture(acc);
}

client.spaces.list()
    .thenCompose(first -> collectAll(first, new ArrayList<>()))
    .thenAccept(all -> System.out.println("Got " + all.size() + " spaces"));

AsyncPage<T> intentionally does not implement Iterable<T> — mixing blocking iteration with async I/O is a footgun. Chain via .next() and .thenCompose() instead.

Async streaming

memories.retrieve returns CompletableFuture<RetrieveMemoryStream> on the async client. The future completes once the stream headers arrive; the returned stream is consumed the same way as in the sync client (lazy Iterable<RetrieveMemoryEvent> inside a try-with-resources block).

RetrieveMemoryRequest req = RetrieveMemoryRequest.builder()
    .message("question")
    .spaceId(spaceId)
    .build();
client.memories.retrieve(req).thenAccept(events -> {
    try (events) {
        for (RetrieveMemoryEvent e : events) process(e);
    }
}).join();

Error handling

HTTP errors (4xx/5xx) and I/O failures complete the future exceptionally with the same exception types as the sync client, wrapped by CompletionException at the call site:

client.memories.get("missing")
    .thenAccept(System.out::println)
    .exceptionally(ex -> {
        Throwable cause = ex.getCause();  // unwrap CompletionException
        if (cause instanceof NotFoundException) {
            System.out.println("Memory not found");
        }
        return null;
    });

Request validation errors — e.g., credential_check throwing when a SaaS endpoint is targeted without credentials — are thrown synchronously, before the HTTP call is scheduled. The rationale: that's a programmer error, not an I/O event, and should fail fast.

Support

Report issues at github.com/PAIR-Systems-Inc/goodmem/issues, or reach out to forrest@pairsys.ai.