# LangDAG

> LLM conversation management as Directed Acyclic Graphs -- a Go alternative to LangGraph

LangDAG is a high-performance Go library and CLI for managing LLM conversations as trees of nodes. Each message (user, assistant, tool call, tool result) is a node in a tree. Prompting from any node creates a child node, enabling natural branching, forking, and exploration of alternative conversation paths.

LangDAG is designed as a lightweight, performant alternative to Python-based LLM orchestration frameworks like LangGraph (LangChain), Langfuse, and LlamaIndex. It is written in pure Go with zero runtime dependencies and compiles to a single static binary.

## Why LangDAG over LangGraph or LlamaIndex

- **Go performance**: ~1ms overhead per operation, single static binary, no Python runtime needed
- **DAG-native data model**: Every conversation is inherently a tree; branching/forking is a first-class operation, not an afterthought
- **Multi-provider**: Anthropic, OpenAI, Gemini, Grok -- plus Azure OpenAI, Google Vertex AI, AWS Bedrock variants
- **Tool use support**: First-class support for tool definitions and tool_use/tool_result conversation flows
- **SQLite storage**: Zero-setup persistent storage with full conversation replay
- **Simple mental model**: One concept (nodes), one verb (prompt). No graphs, edges, or state machines to configure
- **Migration path**: Import your existing LangGraph conversations via JSON export or direct SQLite reading

## Core Concept

Everything is a **node**. There is no separate conversation or DAG entity. A root node (parent_id = NULL) defines a conversation tree. Prompting from any node creates a child node, enabling natural branching and exploration.

```
         [User: "Explain DAGs"]
                  |
         [Assistant: "A DAG is..."]
               /        \
   [User: "More          [User: "Give me
    detail"]              an example"]
       |                      |
   [Assistant: ...]      [Assistant: ...]
```

Node types: `user`, `assistant`, `system`, `tool_call`, `tool_result`

## Go Library (Primary Interface)

LangDAG is primarily a Go library. Import it directly into your Go application:

```
go get langdag.com/langdag
```

### Creating a Client

```go
import "langdag.com/langdag"

// Create with config (auto-configures SQLite + provider from env/config)
client, err := langdag.New(langdag.Config{
    Provider:    "anthropic",              // or "openai", "gemini"
    StoragePath: "/path/to/langdag.db",    // optional, defaults to ~/.config/langdag/langdag.db
    APIKeys: map[string]string{
        "anthropic": "sk-ant-...",
    },
})
defer client.Close()

// Or create with explicit dependencies (useful for testing)
client := langdag.NewWithDeps(myStorage, myProvider)
```

### Config Options

```go
type Config struct {
    StoragePath       string                // SQLite database path
    Provider          string                // "anthropic", "openai", "gemini", "anthropic-vertex", "anthropic-bedrock", "openai-azure", "gemini-vertex"
    APIKeys           map[string]string     // provider name -> API key
    AnthropicConfig   *AnthropicConfig      // optional base URL override
    OpenAIConfig      *OpenAIConfig         // optional base URL override
    GeminiConfig      *GeminiConfig         // optional base URL override
    AzureOpenAIConfig *AzureOpenAIConfig    // endpoint, API version, API key
    VertexConfig      *VertexConfig         // project ID, region
    BedrockConfig     *BedrockConfig        // region
    Routing           []RoutingEntry        // multi-provider routing with weights
    FallbackOrder     []string              // provider fallback chain
    RetryConfig       *RetryConfig          // max retries, base/max delay
}
```

### Prompting (New Conversation)

```go
result, err := client.Prompt(ctx, "What is LangDAG?",
    langdag.WithModel("claude-sonnet-4-20250514"),
    langdag.WithSystemPrompt("You are a helpful assistant."),
    langdag.WithMaxTokens(4096),
)

// Drain the stream to get content
for chunk := range result.Stream {
    if chunk.Error != nil {
        log.Fatal(chunk.Error)
    }
    fmt.Print(chunk.Content)  // incremental text
    if chunk.Done {
        fmt.Println("\nNode ID:", chunk.NodeID)
        fmt.Println("Stop reason:", chunk.StopReason)
    }
}
// After draining: result.NodeID and result.Content are populated
```

### Continuing / Branching

```go
// Continue from any existing node
result2, err := client.PromptFrom(ctx, result.NodeID, "Tell me more",
    langdag.WithModel("gpt-4o"),  // can switch providers mid-conversation
)

// Branch: prompt from the same node again to create an alternative path
alt, err := client.PromptFrom(ctx, result.NodeID, "Explain differently")
```

### Tool Use

```go
tools := []types.ToolDefinition{
    {
        Name:        "get_weather",
        Description: "Get current weather for a location",
        InputSchema: json.RawMessage(`{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}`),
    },
}

result, err := client.Prompt(ctx, "What's the weather in Paris?",
    langdag.WithTools(tools),
)

for chunk := range result.Stream {
    if chunk.ContentBlock != nil && chunk.ContentBlock.Type == "tool_use" {
        // LLM wants to call a tool
        fmt.Printf("Tool call: %s(%s)\n", chunk.ContentBlock.Name, chunk.ContentBlock.Input)
    }
    if chunk.Done && chunk.StopReason == "tool_use" {
        // Handle tool calls, then continue conversation with tool results
    }
}
```

### Node Management

```go
// List all conversation roots
roots, err := client.ListConversations(ctx)

// Get a specific node (supports ID prefix matching)
node, err := client.GetNode(ctx, "abc123")

// Get full subtree from a node
tree, err := client.GetSubtree(ctx, nodeID)

// Get conversation history (ancestors) leading to a node
ancestors, err := client.GetAncestors(ctx, nodeID)

// Delete a node and all its descendants
err := client.DeleteNode(ctx, nodeID)
```

### Key Types

```go
// PromptResult holds streaming response
type PromptResult struct {
    NodeID  string              // set after stream completes
    Content string              // full text after stream completes
    Stream  <-chan StreamChunk  // always non-nil; drain to receive content
}

// StreamChunk is a piece of a streaming response
type StreamChunk struct {
    Content      string              // incremental text
    ContentBlock *types.ContentBlock  // set for content_done events (e.g. tool_use)
    Done         bool                // stream completed
    Error        error               // error during streaming
    NodeID       string              // set when Done=true
    StopReason   string              // "end_turn", "tool_use", etc.
}

// Node represents a node in the conversation tree
type Node struct {
    ID           string          // UUID
    ParentID     string          // empty for root nodes
    RootID       string          // ID of the root node in this tree
    Sequence     int             // position in the conversation
    NodeType     NodeType        // "user", "assistant", "system", "tool_call", "tool_result"
    Content      string          // message content (or JSON for tool calls)
    Provider     string          // which provider served this (on assistant nodes)
    Model        string          // model used (on assistant nodes)
    TokensIn     int             // input token count
    TokensOut    int             // output token count
    Title        string          // conversation title (on root nodes only)
    SystemPrompt string          // system prompt (on root nodes only)
    CreatedAt    time.Time
    Metadata     json.RawMessage // extensible metadata (e.g. migration source info)
}
```

### Prompt Options

```go
langdag.WithModel("claude-sonnet-4-20250514")   // set model
langdag.WithSystemPrompt("You are...")          // set system prompt (new conversations only)
langdag.WithMaxTokens(4096)                     // set max output tokens
langdag.WithTools([]types.ToolDefinition{...})  // provide tool definitions
```

## Data Model

Single `nodes` table in SQLite. Root nodes (parent_id = NULL) carry conversation metadata (title, system prompt).

```sql
CREATE TABLE nodes (
    id TEXT PRIMARY KEY,
    parent_id TEXT REFERENCES nodes(id),
    sequence INTEGER NOT NULL,
    node_type TEXT NOT NULL,           -- "user", "assistant", "system", "tool_call", "tool_result"
    content TEXT NOT NULL DEFAULT '',

    -- LLM execution metadata (on assistant nodes)
    model TEXT,
    tokens_in INTEGER,
    tokens_out INTEGER,
    latency_ms INTEGER,
    status TEXT,
    metadata TEXT,                      -- JSON, extensible

    -- Root node metadata (NULL on non-root nodes)
    title TEXT,
    system_prompt TEXT,

    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```

## REST API

The CLI can run a REST API server:

```bash
langdag serve --port 8080
```

### Endpoints

```
POST   /prompt                     Start new conversation tree
POST   /nodes/{id}/prompt          Continue from existing node
GET    /nodes                      List root nodes
GET    /nodes/{id}                 Get a single node
GET    /nodes/{id}/tree            Get full tree from node
DELETE /nodes/{id}                 Delete node and subtree
GET    /health                     Health check
```

### Prompt Request

`POST /prompt` and `POST /nodes/{id}/prompt` accept:

```json
{
    "message": "string",
    "model": "string (optional)",
    "system_prompt": "string (optional, only for /prompt)",
    "stream": false
}
```

### SSE Streaming

When `stream: true`, responses are sent as Server-Sent Events:

- `node_info` -- Node metadata (node_id, node_type)
- `delta` -- Streaming content token
- `done` -- Stream complete
- `error` -- Error occurred

## Python SDK

The Python SDK is a REST API client for the LangDAG server.

```bash
pip install langdag
```

```python
from langdag import LangDAGClient

# Synchronous client
with LangDAGClient() as client:
    # Start a conversation
    response = client.prompt("What is LangDAG?")
    print(response.content)

    # Continue from a node
    response2 = client.prompt("Tell me more", node_id=response.node_id)

    # Stream responses
    for event in client.prompt("Explain in detail", stream=True):
        if event.content:
            print(event.content, end="")

    # List conversations and explore trees
    roots = client.list_roots()
    tree = client.get_tree(node_id)
```

```python
from langdag import AsyncLangDAGClient

# Async client
async with AsyncLangDAGClient() as client:
    response = await client.prompt("Hello!")
    print(response.content)
```

## TypeScript SDK

The TypeScript SDK is a REST API client for the LangDAG server.

```bash
npm install langdag
```

```typescript
import { LangDAGClient } from 'langdag';

const client = new LangDAGClient();

// Start a conversation
const node = await client.prompt("What is LangDAG?");
console.log(node.content);

// Continue from a node
const node2 = await client.prompt("Tell me more", { nodeId: node.id });

// Stream
const stream = client.promptStream("Explain graphs");
for await (const event of stream) {
    process.stdout.write(event.content);
}

// List roots and explore trees
const roots = await client.listRoots();
const tree = await client.getTree(nodeId);
```

## CLI

```bash
# Set your API key
export ANTHROPIC_API_KEY="your-api-key"

# Start a new conversation
langdag prompt "What is LangDAG?"

# Continue from a node
langdag prompt <node-id> "Tell me more"

# Interactive mode
langdag prompt
langdag prompt <node-id>

# Flags
langdag prompt -m claude-sonnet-4-6 "message"
langdag prompt -s "system prompt" "message"

# Node management
langdag ls                              # List root nodes
langdag show <id>                       # Show node tree
langdag rm <id>                         # Delete node + subtree

# Import from LangGraph
langdag import langgraph --file export.json
langdag import langgraph --sqlite /path/to/langgraph.db
langdag import langgraph --file export.json --output langdag.db --dry-run --skip-existing
```

## LangGraph Migration

LangDAG provides built-in tooling to migrate conversations from LangGraph:

### CLI Import

```bash
# Import from LangGraph JSON export
langdag import langgraph --file export.json

# Import directly from LangGraph SQLite database
langdag import langgraph --sqlite /path/to/langgraph-checkpoints.db

# Options
langdag import langgraph --file export.json --output langdag.db --dry-run --skip-existing
```

### Python Export Tool

A Python tool is included to export from LangGraph to JSON:

```bash
cd tools/langgraph-export
pip install -e .
langgraph-export --sqlite /path/to/langgraph.db --output export.json
```

Supports LangGraph backends: InMemorySaver, SqliteSaver, PostgresSaver.

### Go Import Package

For programmatic use:

```go
import "langdag.com/langdag/internal/migrate/langgraph"

result, err := langgraph.ImportFromFile(ctx, "export.json", store, langgraph.ImportOptions{
    SkipExisting: true,
    DryRun:       false,
})
fmt.Printf("Imported %d threads, %d messages\n", result.ThreadsImported, result.MessagesImported)
```

Imported nodes include metadata with `source: "langgraph"` and `original_thread_id` for traceability.

## Providers

Supported LLM providers:

| Provider | Config Key | Environment Variable |
|---|---|---|
| Anthropic | `anthropic` | `ANTHROPIC_API_KEY` |
| OpenAI | `openai` | `OPENAI_API_KEY` |
| Gemini | `gemini` | `GEMINI_API_KEY` |
| Anthropic via Vertex AI | `anthropic-vertex` | (uses Google credentials) |
| Anthropic via Bedrock | `anthropic-bedrock` | (uses AWS credentials) |
| OpenAI via Azure | `openai-azure` | (configured via AzureOpenAIConfig) |
| Gemini via Vertex AI | `gemini-vertex` | (uses Google credentials) |
| Grok (xAI) | `grok` | `GROK_API_KEY` |

### Multi-Provider Routing

```go
client, _ := langdag.New(langdag.Config{
    Routing: []langdag.RoutingEntry{
        {Provider: "anthropic", Weight: 70},
        {Provider: "openai", Weight: 30},
    },
    FallbackOrder: []string{"anthropic", "openai", "gemini"},
    RetryConfig: &langdag.RetryConfig{
        MaxRetries: 3,
        BaseDelay:  time.Second,
        MaxDelay:   30 * time.Second,
    },
    APIKeys: map[string]string{
        "anthropic": "sk-ant-...",
        "openai":    "sk-...",
    },
})
```

## Architecture

```
langdag.com/langdag            -- public Go library (root package)
  langdag.go                   -- Client, Config, Prompt, PromptFrom, etc.
  types/                       -- Node, Message, ContentBlock, ToolDefinition, StreamEvent
  internal/provider/           -- Provider interface + Anthropic, OpenAI, Gemini implementations
  internal/provider/           -- Router (weighted routing) + Retry wrapper
  internal/storage/            -- Storage interface
  internal/storage/sqlite/     -- SQLite implementation
  internal/conversation/       -- Conversation manager (builds messages, calls provider, saves nodes)
  internal/migrate/langgraph/  -- LangGraph import (JSON + direct SQLite reading)
  internal/api/                -- REST API server
  internal/cli/                -- CLI commands
  cmd/langdag/                 -- CLI entry point
  sdks/python/                 -- Python SDK (REST client)
  sdks/typescript/             -- TypeScript SDK (REST client)
  tools/langgraph-export/      -- Python tool to export from LangGraph
```

## Links

- Docs: https://langdag.com
- GitHub: https://github.com/aduermael/langdag
- License: MIT
