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.

This guide keeps the example history-based. Trusted runtime clients can also fork at client-visible journal entries for replay/debug workflows; see Forking for the channel rules.

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);

What changed: Added a listConversationForks endpoint at GET /{conversationId}/forks that calls proxy.listConversationForks. Why: Frontends receive all accessible group IDs plus fork points keyed to entries visible in the requested conversation, without loading entries from sibling branches.

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:

{
  "conversationIds": [
    "88a1db4c-ce91-4391-a6c1-baf9bcc65a86",
    "d517e108-28f0-49a1-93cc-d0f48ebedf42"
  ],
  "forkPoints": [
    {
      "entryId": "482d2e0c-1677-4ec5-91f2-fb3cf31deeda",
      "options": [
        {
          "conversationId": "88a1db4c-ce91-4391-a6c1-baf9bcc65a86",
          "entryId": "482d2e0c-1677-4ec5-91f2-fb3cf31deeda",
          "title": "A conversation",
          "createdAt": "2026-07-10T14:00:00Z"
        },
        {
          "conversationId": "d517e108-28f0-49a1-93cc-d0f48ebedf42",
          "entryId": "dd56872d-dba2-4275-be85-dc431169737f",
          "title": "Forked conversation",
          "createdAt": "2026-07-10T14:01:00Z"
        }
      ]
    }
  ]
}

Next Steps