Conversation Forking

This guide covers conversation forking — letting users branch off from any point in a conversation to explore alternative paths.

New to forking concepts? Read Forking first to understand how conversation forking works. This guide focuses on the Quarkus implementation.

Prerequisites

Starting checkpoint: This guide starts from java/quarkus/examples/doc-checkpoints/03-with-history

Make sure you’ve completed the previous guides:

Conversation Forking

How Forking Works

Forks are created implicitly when the first entry is appended to a new conversation with fork metadata. In this checkpoint, HistoryRecordingAgent remains the same as the previous step, and fork metadata is passed when creating entries through the Memory Service API:

HistoryRecordingAgent.java
package org.acme;

import io.github.chirino.memory.history.annotations.ConversationId;
import io.github.chirino.memory.history.annotations.ForkedAtConversationId;
import io.github.chirino.memory.history.annotations.ForkedAtEntryId;
import io.github.chirino.memory.history.annotations.RecordConversation;
import io.github.chirino.memory.history.annotations.UserMessage;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

@ApplicationScoped
public class HistoryRecordingAgent {

    private final Agent agent;

    @Inject
    public HistoryRecordingAgent(Agent agent) {
        this.agent = agent;
    }

    @RecordConversation
    public String chat(
            @ConversationId String conversationId,
            @UserMessage String userMessage,
            @ForkedAtConversationId String forkedAtConversationId,
            @ForkedAtEntryId String forkedAtEntryId) {
        return agent.chat(conversationId, userMessage);
    }
}

Why: Forking is initiated at the history entry level, not at the agent level. When the first entry in a new conversation includes forkedAtConversationId and forkedAtEntryId fields, the Memory Service automatically establishes the fork relationship and replays the parent conversation’s memory up to the fork point.

Listing Forks

Add this method to ConversationsResource.java to list forks for a conversation:

ConversationsResource.java
    @Path("/{conversationId}/forks")
    @Produces(MediaType.APPLICATION_JSON)
    public Response listConversationForks(@PathParam("conversationId") String conversationId) {
        return proxy.listConversationForks(conversationId, null, null);

What changed: Added a listConversationForks endpoint at GET /{conversationId}/forks that calls proxy.listConversationForks. Why: Frontends need to discover all branches stemming from a conversation so users can navigate between them. The endpoint returns the root conversation and all its forks in a flat list, each annotated with its forkedAtConversationId so the UI can reconstruct the branch tree.

Try It With Curl

Define a helper to get a bearer token for bob:

function get-token() {
  curl -sSfX POST http://localhost:8081/realms/memory-service/protocol/openid-connect/token \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "client_id=memory-service-client" \
    -d "client_secret=change-me" \
    -d "grant_type=password" \
    -d "username=bob" \
    -d "password=bob" \
    | jq -r '.access_token'
}

Create a turn on the source conversation:

curl -NsSfX POST http://localhost:9090/chat/88a1db4c-ce91-4391-a6c1-baf9bcc65a86 \
  -H "Content-Type: text/plain" \
  -H "Authorization: Bearer $(get-token)" \
  -d "Hello from the root conversation."

Example output:

Sure, I can help with that.

Fetch the entry id to fork from:

curl -sSfX GET http://localhost:9090/v1/conversations/88a1db4c-ce91-4391-a6c1-baf9bcc65a86/entries \
  -H "Authorization: Bearer $(get-token)" | jq

Example output:

{
  "data": [
    {
      "id": "04f83525-aa03-4beb-ac93-c33ec9c353b4"
    },
    {
      "id": "5fe98b13-17c6-4b19-bb49-a8b44da51927"
    }
  ],
  "afterCursor": null
}

Create the forked conversation by calling chat with fork metadata:

curl -NsSfX POST "http://localhost:9090/chat/d517e108-28f0-49a1-93cc-d0f48ebedf42?forkedAtConversationId=88a1db4c-ce91-4391-a6c1-baf9bcc65a86&forkedAtEntryId=${FORK_ENTRY_ID}" \
  -H "Content-Type: text/plain" \
  -H "Authorization: Bearer $(get-token)" \
  -d "Continue from this fork."

Example output:

{
  "data": [
    {
      "id": "04f83525-aa03-4beb-ac93-c33ec9c353b4",
      "conversationId": "88a1db4c-ce91-4391-a6c1-baf9bcc65a86",
      "userId": "bob",
      "channel": "history",
      "contentType": "history",
      "content": [
        {
          "role": "USER",
          "text": "Hello from the root conversation."
        }
      ],
      "createdAt": "2026-03-06T14:59:00.689985Z"
    },
    {
      "id": "5fe98b13-17c6-4b19-bb49-a8b44da51927",
      "conversationId": "88a1db4c-ce91-4391-a6c1-baf9bcc65a86",
      "userId": "bob",
      "channel": "history",
      "contentType": "history",
      "content": [
        {
          "role": "AI",
          "text": "Hello! How can I assist you today?"
        }
      ],
      "createdAt": "2026-03-06T14:59:01.427801Z"
    }
  ]
}

List forks for the source conversation through the Quarkus proxy endpoint:

curl -sSfX GET http://localhost:9090/v1/conversations/88a1db4c-ce91-4391-a6c1-baf9bcc65a86/forks \
  -H "Authorization: Bearer $(get-token)" | jq

Example output:

{
  "data": [
    {
      "conversationId": "88a1db4c-ce91-4391-a6c1-baf9bcc65a86"
    },
    {
      "conversationId": "d517e108-28f0-49a1-93cc-d0f48ebedf42"
    }
  ],
  "afterCursor": null
}

Next Steps