cd /news/large-language-models/branch-agent-git-style-branching-for… Β· home β€Ί topics β€Ί large-language-models β€Ί article
[ARTICLE Β· art-42622] src=dev.to β†— pub= topic=large-language-models verified=true sentiment=↑ positive

Branch Agent: Git-Style Branching for LLM Conversations

A developer built Branch Agent, a system that applies Git-style branching to LLM conversations, enabling users to fork, branch, and merge AI conversations with different models, prompts, and providers in each parallel timeline. The system uses Convex for database operations and Agno for agent management, with O(1) storage for forks via pointer references.

read5 min views1 publishedJun 28, 2026

Fork, branch, and merge AI conversations like code β€” with different models, prompts, and providers in each parallel timeline.

When experimenting with LLMs, you've probably done this: tweak the system prompt, rerun the conversation, compare outputs side-by-side in two browser tabs, manually copy-paste the better response, and repeat. It's messy, non-reproducible, and there's no version history.

What if LLM conversations had the same branching model as Git?

A conversation is a tree, not a list. Every message can be a fork point. New branches inherit the full context up to that point via pointer references (O(1), no copying). Each branch gets its own agent configuration β€” system prompt, model, provider, temperature, tools. Branches can be compared side-by-side and merged back via an AI "Judge Agent."

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      Convex hooks       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Next.js UI  β”‚ ◄─────────────────────► β”‚  Convex Database β”‚
β”‚  (React 19)  β”‚     useQuery/mutation   β”‚  (workspaces,    β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜                         β”‚   branches, msgs)β”‚
       β”‚ HTTP POST /chat (SSE stream)    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Python FastAPI  β”‚  ← Agno Agent SDK
β”‚  Agno Service    β”‚     Creates agent per request with branch config
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚ OpenAI-compatible API
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Any LLM Providerβ”‚  ← OpenAI, Together, Groq, Ollama...
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Convex provides:

forkBranch

is atomic and isolatedchatWithAgent

action runs outside the transaction but can call internal queries/mutations safelyAgno is a Python-native agent framework that supports:

The schema is deliberately relational to support tree traversal:

// convex/schema.ts
export const agentConfigSchema = v.object({
  systemPrompt: v.optional(v.string()),
  model: v.optional(v.string()),
  baseUrl: v.optional(v.string()),   // per-branch provider URL
  apiKey: v.optional(v.string()),    // per-branch API key
  tools: v.optional(v.array(v.string())),
  temperature: v.optional(v.number()),
  maxTokens: v.optional(v.number()),
});

export const branches = defineTable({
  workspaceId: v.id("workspaces"),
  name: v.string(),
  parentBranchId: v.optional(v.id("branches")),
  snapshotMessageId: v.optional(v.id("messages")),
  agentConfig: v.optional(agentConfigSchema),
  isMerged: v.optional(v.boolean()),
  mergeSummary: v.optional(v.string()),
})
  .index("by_workspace", ["workspaceId"])
  .index("by_parent", ["parentBranchId"]);

export const messages = defineTable({
  branchId: v.id("branches"),
  parentMessageId: v.optional(v.id("messages")),
  role: v.union(v.literal("user"), v.literal("assistant"), v.literal("system")),
  content: v.string(),
  metadata: v.optional(messageMetadataSchema),
})
  .index("by_branch_created", ["branchId", "createdAt"]);

The key insight: snapshotMessageId

on a branch points to the message where it forked. History reconstruction walks parentBranchId

β†’ snapshotMessageId

pointers recursively. This makes forks O(1) in storage β€” no message duplication.

export const forkBranch = mutation({
  args: {
    sourceBranchId: v.id("branches"),
    snapshotMessageId: v.id("messages"),
    newBranchName: v.string(),
    agentConfig: v.optional(agentConfigSchema),
  },
  handler: async (ctx, args) => {
    // Just create a new branch with pointer references
    // No messages are copied
    return await ctx.db.insert("branches", {
      workspaceId: sourceBranch.workspaceId,
      name: args.newBranchName,
      parentBranchId: args.sourceBranchId,
      snapshotMessageId: args.snapshotMessageId,
      agentConfig: args.agentConfig ?? sourceBranch.agentConfig,
      createdAt: Date.now(),
    });
  },
});

When a user sends a message, the action fetches the full context by walking the branch tree:

export const internalGetBranchHistory = internalQuery({
  handler: async (ctx, args) => {
    const branch = await ctx.db.get(args.branchId);
    const myMessages = await ctx.db
      .query("messages")
      .withIndex("by_branch_created", (q) => q.eq("branchId", args.branchId))
      .order("asc")
      .collect();

    if (!branch.parentBranchId || !branch.snapshotMessageId) {
      return myMessages;  // root branch, just our messages
    }

    // Walk up the parent tree to the snapshot point
    const parentMessages = await traverseToSnapshot(
      ctx, branch.parentBranchId, branch.snapshotMessageId
    );
    return [...parentMessages, ...myMessages];
  },
});

The chatWithAgent

action sends the full history to the Python Agno service, which streams tokens back via SSE:

// Convex action reads branch config, sends to Agno service
const agnoPayload = {
  messages: conversationMessages,
  system_prompt: branch.agentConfig?.systemPrompt,
  model: branch.agentConfig?.model,
  base_url: branch.agentConfig?.baseUrl,
  api_key: branch.agentConfig?.apiKey,
  tools: branch.agentConfig?.tools,
  temperature: branch.agentConfig?.temperature,
  stream: true,
};

// Parse SSE events and update message content in real-time
for await (const sseEvent of sseReader) {
  if (parsed.type === "content" && parsed.content) {
    fullContent += parsed.content;
    await ctx.runMutation(internalUpdateMessageStream, {
      messageId: assistantMessageId,
      content: fullContent,
    });
  }
}

Each token delta updates the Convex document, which triggers the reactive useQuery

hook on the frontend β€” the UI streams the response smoothly.

def create_agent(
    system_prompt: str = None,
    model_name: str = None,
    base_url: str = None,
    api_key: str = None,
    tool_names: list[str] = None,
    temperature: float = None,
    max_tokens: int = None,
) -> Agent:
    model = _resolve_model(
        model_name or AGNO_DEFAULT_MODEL,
        temperature, max_tokens,
        base_url=base_url, api_key=api_key,
    )
    tools = _resolve_tools(tool_names or [])
    return Agent(
        model=model,
        tools=tools or None,
        instructions=[system_prompt] if system_prompt else None,
    )

The service creates a fresh Agent

per request β€” no state leakage between branches. Each branch can point to a completely different provider.

The compare view lets you see two branches at the same time. This is particularly useful when testing different system prompts or models against the same conversation history:

<CompareView
  leftBranch={{ name: "main (GPT-4o)", messages }}
  rightBranch={{ name: "fork (Llama)", messages: compareMessages }}
/>

Both panels scroll independently, and messages from forked history are tagged with their source branch name.

When a fork has answered its questions, you can merge it back into the parent branch. The mergeWithJudge

action sends both branches' histories to an Agno agent with a "judge" prompt that summarizes the key learnings:

You are a merge judge. Summarize the key learnings, differences, and insights
from the SOURCE branch, and produce a concise merge summary.

The summary is inserted as a system message in the target branch, and the source branch is marked as merged.

npx convex dev                    # Convex backend
cd agno_service && python3 main.py  # Agno Python service
npm run dev                       # Next.js frontend

Or one command: ./start.sh

This project is a cookbook / reference implementation. Here are ideas for extending it:

Component Technology Why
Database Convex Reactive WebSockets, ACID, type-safe
Agent SDK Agno (Python) Multi-provider, streaming, tools
Frontend Next.js 15 + React 19 App Router, server components
Styling Tailwind CSS v4 + shadcn/ui Utility-first, dark mode
AI Provider Any OpenAI-compatible API BYO provider

Code & more: https://www.dailybuild.xyz/project/177-convex-branch-agent

── more in #large-language-models 4 stories Β· sorted by recency
── more on @convex 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/branch-agent-git-sty…] indexed:0 read:5min 2026-06-28 Β· β€”