Quarkus REST Client

The Quarkus extension provides two ways to interact with Memory Service via REST:

  1. MemoryServiceProxy - A helper class for building JAX-RS proxy endpoints that forward requests to the memory service
  2. Generated API Clients - Type-safe REST clients for direct programmatic access

Setup

The REST client is included in the extension:

<dependency>
  <groupId>io.github.chirino.memory-service</groupId>
  <artifactId>memory-service-extension</artifactId>
  <version>999-SNAPSHOT</version>
</dependency>

Configuration

# Memory Service URL (auto-configured with Dev Services)
memory-service.client.url=http://localhost:8080

# API key for agent authentication
memory-service.client.api-key=your-api-key

The extension uses these properties directly when it builds generated REST clients.

Calling as an asserted user

An API-key client listed in the server’s MEMORY_SERVICE_TRUSTED_USER_ID_CLIENTS can call normal user APIs by attaching X-User-ID with its API key. Create the filtered client for the request so concurrent users do not share mutable header state:

import io.github.chirino.memory.client.api.ConversationsApi;
import jakarta.ws.rs.client.ClientRequestFilter;
import java.net.URI;
import org.eclipse.microprofile.rest.client.RestClientBuilder;

ConversationsApi conversationsFor(String userId) {
    return RestClientBuilder.newBuilder()
        .baseUri(URI.create("http://memory-service:8080"))
        .register((ClientRequestFilter) request -> {
            request.getHeaders().putSingle("X-API-Key", serviceApiKey);
            request.getHeaders().putSingle("X-User-ID", userId);
        })
        .build(ConversationsApi.class);
}

X-User-ID is not authentication. Untrusted clients have the header ignored, while invalid credentials are still rejected. Admin and system APIs also ignore it. See Trusted Client User Identity Assertion for server configuration and OIDC behavior.

Using MemoryServiceProxy

The MemoryServiceProxy is a helper class that makes it easier to implement JAX-RS endpoints that proxy requests to the memory service. It handles authentication and bearer token propagation automatically.

Injecting the Proxy

import io.github.chirino.memory.runtime.MemoryServiceProxy;

@ApplicationScoped
public class ConversationsResource {

    @Inject
    MemoryServiceProxy proxy;
}

Conversations API

import io.github.chirino.memory.client.model.Channel;
import jakarta.ws.rs.core.Response;
import java.util.List;

// List conversations
Response response = proxy.listConversations(mode, ancestry, afterCursor, limit, query);

// Filter conversations by metadata (up to 5 predicates, combined with AND)
Response filtered = proxy.listConversations(
    "all",          // mode
    null,           // ancestry
    null,           // afterCursor
    20,             // limit
    null,           // query
    "exclude",      // archived
    List.of("status=waiting", "agent-id!=worker-2")  // metadata filters
);

// Get a conversation
Response response = proxy.getConversation(conversationId);

// Create a conversation (body is JSON string)
Response response = proxy.createConversation(jsonBody);

// Delete a conversation
Response response = proxy.deleteConversation(conversationId);

// List conversation forks
Response response = proxy.listConversationForks(conversationId);

Entries API

// List conversation entries
// channel: Channel.HISTORY for user-visible messages, Channel.CONTEXT for agent context
// epoch: "latest", "all", or a numeric epoch identifier
// forks: "none" or "all"
Response response = proxy.listConversationEntries(
    conversationId, after, limit, Channel.HISTORY, epoch, forks);

// Append an entry (body is JSON string)
Response response = proxy.appendConversationEntry(conversationId, jsonBody);

Sharing API

// List memberships
Response response = proxy.listConversationMemberships(conversationId);

// Share a conversation (body is JSON string with userId and accessLevel)
Response response = proxy.shareConversation(conversationId, jsonBody);

// Update membership
Response response = proxy.updateConversationMembership(conversationId, userId, jsonBody);

// Remove membership
Response response = proxy.deleteConversationMembership(conversationId, userId);

Ownership Transfers

// List pending transfers
Response response = proxy.listPendingTransfers(role);

// Create ownership transfer
Response response = proxy.createOwnershipTransfer(jsonBody);

// Get transfer details
Response response = proxy.getTransfer(transferId);

// Accept transfer
Response response = proxy.acceptTransfer(transferId);

// Delete/cancel transfer
Response response = proxy.deleteTransfer(transferId);

Search API

// Search conversations (body is JSON SearchConversationsRequest)
Response response = proxy.searchConversations(jsonBody);

// Index entries (body is JSON array of IndexEntryRequest)
Response response = proxy.indexConversations(jsonBody);

Response Cancellation

// Cancel an in-progress response
Response response = proxy.cancelResponse(conversationId);

Using Generated API Clients

For direct programmatic access without JAX-RS proxying, you can use the generated API clients with MemoryServiceApiBuilder:

import io.github.chirino.memory.runtime.MemoryServiceApiBuilder;
import io.github.chirino.memory.client.api.ConversationsApi;
import io.github.chirino.memory.client.api.SearchApi;
import io.github.chirino.memory.client.api.SharingApi;
import io.github.chirino.memory.client.model.*;

@ApplicationScoped
public class MyService {

    @Inject
    MemoryServiceApiBuilder apiBuilder;

    public Conversation getConversation(String bearerToken, String conversationId) {
        ConversationsApi api = apiBuilder
            .withBearerAuth(bearerToken)
            .build(ConversationsApi.class);

        return api.getConversation(conversationId);
    }

    public ListConversations200Response listConversations(String bearerToken) {
        ConversationsApi api = apiBuilder
            .withBearerAuth(bearerToken)
            .build(ConversationsApi.class);

        return api.listConversations(
            "latest-fork",  // mode
            null,           // ancestry
            null,           // afterCursor
            20,             // limit
            null,           // query
            "exclude",      // archived
            null            // metadata
        );
    }

    public ListConversationEntries200Response listEntries(
            String bearerToken, String conversationId) {
        ConversationsApi api = apiBuilder
            .withBearerAuth(bearerToken)
            .build(ConversationsApi.class);

        return api.listConversationEntries(
            conversationId,
            null,               // after cursor
            50,                 // limit
            Channel.HISTORY,    // channel
            "latest",           // epoch
            "none"              // forks
        );
    }
}

Available API Classes

API ClassDescription
ConversationsApiCRUD operations for conversations and entries
SearchApiSemantic search and indexing
SharingApiMemberships and ownership transfers

Error Handling

import jakarta.ws.rs.WebApplicationException;

try {
    Response response = proxy.getConversation(conversationId);
    if (response.getStatus() == 404) {
        // Conversation not found
    }
} catch (WebApplicationException e) {
    int status = e.getResponse().getStatus();
    // Handle HTTP errors
}

Full Resource Example

Here’s a complete JAX-RS resource using MemoryServiceProxy:

package org.acme;

import io.github.chirino.memory.client.model.Channel;
import io.github.chirino.memory.runtime.MemoryServiceProxy;
import io.smallrye.common.annotation.Blocking;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;

@Path("/v1/conversations")
@ApplicationScoped
@Blocking
public class ConversationsResource {

    @Inject
    MemoryServiceProxy proxy;

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Response listConversations(
            @QueryParam("mode") String mode,
            @QueryParam("ancestry") String ancestry,
            @QueryParam("afterCursor") String afterCursor,
            @QueryParam("limit") Integer limit,
            @QueryParam("query") String query) {
        return proxy.listConversations(mode, ancestry, afterCursor, limit, query);
    }

    @GET
    @Path("/{conversationId}")
    @Produces(MediaType.APPLICATION_JSON)
    public Response getConversation(
            @PathParam("conversationId") String conversationId) {
        return proxy.getConversation(conversationId);
    }

    @GET
    @Path("/{conversationId}/entries")
    @Produces(MediaType.APPLICATION_JSON)
    public Response listEntries(
            @PathParam("conversationId") String conversationId,
            @QueryParam("after") String after,
            @QueryParam("limit") Integer limit) {
        return proxy.listConversationEntries(
            conversationId, after, limit, Channel.HISTORY, null, null);
    }

    @DELETE
    @Path("/{conversationId}")
    public Response deleteConversation(
            @PathParam("conversationId") String conversationId) {
        return proxy.deleteConversation(conversationId);
    }
}

Next Steps