{"slug": "branch-agent-git-style-branching-for-llm-conversations", "title": "Branch Agent: Git-Style Branching for LLM Conversations", "summary": "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.", "body_md": "**Fork, branch, and merge AI conversations like code — with different models, prompts, and providers in each parallel timeline.**\n\nWhen 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.\n\nWhat if LLM conversations had the same branching model as Git?\n\nA 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.\"\n\n```\n┌──────────────┐      Convex hooks       ┌──────────────────┐\n│  Next.js UI  │ ◄─────────────────────► │  Convex Database │\n│  (React 19)  │     useQuery/mutation   │  (workspaces,    │\n└──────┬───────┘                         │   branches, msgs)│\n       │ HTTP POST /chat (SSE stream)    └──────────────────┘\n       ▼\n┌──────────────────┐\n│  Python FastAPI  │  ← Agno Agent SDK\n│  Agno Service    │     Creates agent per request with branch config\n└──────┬───────────┘\n       │ OpenAI-compatible API\n       ▼\n┌──────────────────┐\n│  Any LLM Provider│  ← OpenAI, Together, Groq, Ollama...\n└──────────────────┘\n```\n\nConvex provides:\n\n`forkBranch`\n\nis atomic and isolated`chatWithAgent`\n\naction runs outside the transaction but can call internal queries/mutations safelyAgno is a Python-native agent framework that supports:\n\nThe schema is deliberately relational to support tree traversal:\n\n``` js\n// convex/schema.ts\nexport const agentConfigSchema = v.object({\n  systemPrompt: v.optional(v.string()),\n  model: v.optional(v.string()),\n  baseUrl: v.optional(v.string()),   // per-branch provider URL\n  apiKey: v.optional(v.string()),    // per-branch API key\n  tools: v.optional(v.array(v.string())),\n  temperature: v.optional(v.number()),\n  maxTokens: v.optional(v.number()),\n});\n\nexport const branches = defineTable({\n  workspaceId: v.id(\"workspaces\"),\n  name: v.string(),\n  parentBranchId: v.optional(v.id(\"branches\")),\n  snapshotMessageId: v.optional(v.id(\"messages\")),\n  agentConfig: v.optional(agentConfigSchema),\n  isMerged: v.optional(v.boolean()),\n  mergeSummary: v.optional(v.string()),\n})\n  .index(\"by_workspace\", [\"workspaceId\"])\n  .index(\"by_parent\", [\"parentBranchId\"]);\n\nexport const messages = defineTable({\n  branchId: v.id(\"branches\"),\n  parentMessageId: v.optional(v.id(\"messages\")),\n  role: v.union(v.literal(\"user\"), v.literal(\"assistant\"), v.literal(\"system\")),\n  content: v.string(),\n  metadata: v.optional(messageMetadataSchema),\n})\n  .index(\"by_branch_created\", [\"branchId\", \"createdAt\"]);\n```\n\nThe key insight: `snapshotMessageId`\n\non a branch points to the message where it forked. History reconstruction walks `parentBranchId`\n\n→ `snapshotMessageId`\n\npointers recursively. This makes forks **O(1) in storage** — no message duplication.\n\n``` js\nexport const forkBranch = mutation({\n  args: {\n    sourceBranchId: v.id(\"branches\"),\n    snapshotMessageId: v.id(\"messages\"),\n    newBranchName: v.string(),\n    agentConfig: v.optional(agentConfigSchema),\n  },\n  handler: async (ctx, args) => {\n    // Just create a new branch with pointer references\n    // No messages are copied\n    return await ctx.db.insert(\"branches\", {\n      workspaceId: sourceBranch.workspaceId,\n      name: args.newBranchName,\n      parentBranchId: args.sourceBranchId,\n      snapshotMessageId: args.snapshotMessageId,\n      agentConfig: args.agentConfig ?? sourceBranch.agentConfig,\n      createdAt: Date.now(),\n    });\n  },\n});\n```\n\nWhen a user sends a message, the action fetches the full context by walking the branch tree:\n\n``` js\nexport const internalGetBranchHistory = internalQuery({\n  handler: async (ctx, args) => {\n    const branch = await ctx.db.get(args.branchId);\n    const myMessages = await ctx.db\n      .query(\"messages\")\n      .withIndex(\"by_branch_created\", (q) => q.eq(\"branchId\", args.branchId))\n      .order(\"asc\")\n      .collect();\n\n    if (!branch.parentBranchId || !branch.snapshotMessageId) {\n      return myMessages;  // root branch, just our messages\n    }\n\n    // Walk up the parent tree to the snapshot point\n    const parentMessages = await traverseToSnapshot(\n      ctx, branch.parentBranchId, branch.snapshotMessageId\n    );\n    return [...parentMessages, ...myMessages];\n  },\n});\n```\n\nThe `chatWithAgent`\n\naction sends the full history to the Python Agno service, which streams tokens back via SSE:\n\n``` js\n// Convex action reads branch config, sends to Agno service\nconst agnoPayload = {\n  messages: conversationMessages,\n  system_prompt: branch.agentConfig?.systemPrompt,\n  model: branch.agentConfig?.model,\n  base_url: branch.agentConfig?.baseUrl,\n  api_key: branch.agentConfig?.apiKey,\n  tools: branch.agentConfig?.tools,\n  temperature: branch.agentConfig?.temperature,\n  stream: true,\n};\n\n// Parse SSE events and update message content in real-time\nfor await (const sseEvent of sseReader) {\n  if (parsed.type === \"content\" && parsed.content) {\n    fullContent += parsed.content;\n    await ctx.runMutation(internalUpdateMessageStream, {\n      messageId: assistantMessageId,\n      content: fullContent,\n    });\n  }\n}\n```\n\nEach token delta updates the Convex document, which triggers the reactive `useQuery`\n\nhook on the frontend — the UI streams the response smoothly.\n\n``` python\n# agno_service/agent_handler.py\ndef create_agent(\n    system_prompt: str = None,\n    model_name: str = None,\n    base_url: str = None,\n    api_key: str = None,\n    tool_names: list[str] = None,\n    temperature: float = None,\n    max_tokens: int = None,\n) -> Agent:\n    model = _resolve_model(\n        model_name or AGNO_DEFAULT_MODEL,\n        temperature, max_tokens,\n        base_url=base_url, api_key=api_key,\n    )\n    tools = _resolve_tools(tool_names or [])\n    return Agent(\n        model=model,\n        tools=tools or None,\n        instructions=[system_prompt] if system_prompt else None,\n    )\n```\n\nThe service creates a fresh `Agent`\n\nper request — no state leakage between branches. Each branch can point to a completely different provider.\n\nThe 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:\n\n```\n<CompareView\n  leftBranch={{ name: \"main (GPT-4o)\", messages }}\n  rightBranch={{ name: \"fork (Llama)\", messages: compareMessages }}\n/>\n```\n\nBoth panels scroll independently, and messages from forked history are tagged with their source branch name.\n\nWhen a fork has answered its questions, you can merge it back into the parent branch. The `mergeWithJudge`\n\naction sends both branches' histories to an Agno agent with a \"judge\" prompt that summarizes the key learnings:\n\n```\nYou are a merge judge. Summarize the key learnings, differences, and insights\nfrom the SOURCE branch, and produce a concise merge summary.\n```\n\nThe summary is inserted as a system message in the target branch, and the source branch is marked as merged.\n\n```\n# Three terminals:\nnpx convex dev                    # Convex backend\ncd agno_service && python3 main.py  # Agno Python service\nnpm run dev                       # Next.js frontend\n```\n\nOr one command: `./start.sh`\n\nThis project is a cookbook / reference implementation. Here are ideas for extending it:\n\n| Component | Technology | Why |\n|---|---|---|\n| Database | Convex | Reactive WebSockets, ACID, type-safe |\n| Agent SDK | Agno (Python) | Multi-provider, streaming, tools |\n| Frontend | Next.js 15 + React 19 | App Router, server components |\n| Styling | Tailwind CSS v4 + shadcn/ui | Utility-first, dark mode |\n| AI Provider | Any OpenAI-compatible API | BYO provider |\n\nCode & more: [https://www.dailybuild.xyz/project/177-convex-branch-agent](https://www.dailybuild.xyz/project/177-convex-branch-agent)", "url": "https://wpnews.pro/news/branch-agent-git-style-branching-for-llm-conversations", "canonical_source": "https://dev.to/harishkotra/branch-agent-git-style-branching-for-llm-conversations-5c61", "published_at": "2026-06-28 15:04:55+00:00", "updated_at": "2026-06-28 15:34:17.052304+00:00", "lang": "en", "topics": ["large-language-models", "developer-tools", "ai-agents", "generative-ai"], "entities": ["Convex", "Agno", "OpenAI", "Together", "Groq", "Ollama", "Next.js", "FastAPI"], "alternates": {"html": "https://wpnews.pro/news/branch-agent-git-style-branching-for-llm-conversations", "markdown": "https://wpnews.pro/news/branch-agent-git-style-branching-for-llm-conversations.md", "text": "https://wpnews.pro/news/branch-agent-git-style-branching-for-llm-conversations.txt", "jsonld": "https://wpnews.pro/news/branch-agent-git-style-branching-for-llm-conversations.jsonld"}}