From ff9a6af07e62afd96bf1e578fa2d2c8ae6e19560 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 05:15:26 +0000 Subject: [PATCH] =?UTF-8?q?docs:=20add=20transcode=20inventory=20=E2=80=94?= =?UTF-8?q?=20Python=20LangGraph=20=E2=86=92=20Rust=20mapping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 5 documents mapping every Python LangGraph module, class, and function to Rust equivalents in rs-graph-llm/graph-flow: - LANGGRAPH_FULL_INVENTORY.md: 132 Python items mapped (46 done, 86 missing, 35% coverage) - LANGGRAPH_PARITY_CHECKLIST.md: prioritized gap analysis (P0-P3) - LANGGRAPH_CRATE_STRUCTURE.md: recommended crate layout with Python → Rust module mapping - LANGGRAPH_TRANSCODING_MAP.md: side-by-side code examples for every pattern - LANGGRAPH_OUR_ADDITIONS.md: 13 features we have that Python LangGraph doesn't https://claude.ai/code/session_01AKkBDoAf2Wrsir2o9vpVzn --- .claude/LANGGRAPH_CRATE_STRUCTURE.md | 253 +++++++++++++++++ .claude/LANGGRAPH_FULL_INVENTORY.md | 376 ++++++++++++++++++++++++ .claude/LANGGRAPH_OUR_ADDITIONS.md | 304 ++++++++++++++++++++ .claude/LANGGRAPH_PARITY_CHECKLIST.md | 223 +++++++++++++++ .claude/LANGGRAPH_TRANSCODING_MAP.md | 394 ++++++++++++++++++++++++++ 5 files changed, 1550 insertions(+) create mode 100644 .claude/LANGGRAPH_CRATE_STRUCTURE.md create mode 100644 .claude/LANGGRAPH_FULL_INVENTORY.md create mode 100644 .claude/LANGGRAPH_OUR_ADDITIONS.md create mode 100644 .claude/LANGGRAPH_PARITY_CHECKLIST.md create mode 100644 .claude/LANGGRAPH_TRANSCODING_MAP.md diff --git a/.claude/LANGGRAPH_CRATE_STRUCTURE.md b/.claude/LANGGRAPH_CRATE_STRUCTURE.md new file mode 100644 index 0000000..bf8b004 --- /dev/null +++ b/.claude/LANGGRAPH_CRATE_STRUCTURE.md @@ -0,0 +1,253 @@ +# Recommended Crate Structure + +> How the Rust crates should map to Python LangGraph's package structure. + +--- + +## Python LangGraph Package Layout + +``` +langgraph (pip install langgraph) +├── langgraph.graph # StateGraph, MessageGraph +├── langgraph.pregel # Core execution engine (Pregel) +├── langgraph.channels # State channels +├── langgraph.managed # Managed values +├── langgraph.func # Functional API (@task, @entrypoint) +├── langgraph.types # Shared types (Command, Send, Interrupt, etc.) +├── langgraph.constants # START, END +├── langgraph.errors # Error types +├── langgraph.config # Config utilities +├── langgraph.runtime # Runtime context +├── langgraph._internal # Private implementation details + +langgraph-prebuilt (pip install langgraph-prebuilt) +├── langgraph.prebuilt.chat_agent_executor # create_react_agent +├── langgraph.prebuilt.tool_node # ToolNode, tools_condition +├── langgraph.prebuilt.tool_validator # ValidationNode +├── langgraph.prebuilt.interrupt # HumanInterrupt types + +langgraph-checkpoint (pip install langgraph-checkpoint) +├── langgraph.checkpoint.base # BaseCheckpointSaver, Checkpoint +├── langgraph.checkpoint.memory # MemorySaver +├── langgraph.checkpoint.serde # Serialization (jsonplus, msgpack) +├── langgraph.store.base # BaseStore, Item +├── langgraph.store.memory # InMemoryStore +├── langgraph.cache # BaseCache, InMemoryCache + +langgraph-checkpoint-postgres +├── langgraph.checkpoint.postgres # PostgresSaver +├── langgraph.store.postgres # PostgresStore + +langgraph-checkpoint-sqlite +├── langgraph.checkpoint.sqlite # SqliteSaver +├── langgraph.store.sqlite # SqliteStore +``` + +--- + +## Current Rust Crate Layout + +``` +rs-graph-llm/ +├── graph-flow/ # Core framework (≈ langgraph + langgraph-prebuilt) +│ ├── src/ +│ │ ├── lib.rs # Re-exports +│ │ ├── graph.rs # Graph, GraphBuilder, edges +│ │ ├── task.rs # Task trait, NextAction, TaskResult +│ │ ├── context.rs # Context, ChatHistory +│ │ ├── error.rs # GraphError +│ │ ├── storage.rs # Session, SessionStorage trait, InMemory +│ │ ├── runner.rs # FlowRunner +│ │ ├── streaming.rs # StreamingRunner, StreamChunk, StreamMode +│ │ ├── compat.rs # StateGraph, START/END, Command +│ │ ├── subgraph.rs # SubgraphTask +│ │ ├── fanout.rs # FanOutTask (≈ Send) +│ │ ├── typed_context.rs # TypedContext +│ │ ├── channels.rs # Channels, ChannelReducer +│ │ ├── retry.rs # RetryPolicy, BackoffStrategy +│ │ ├── run_config.rs # RunConfig, BreakpointConfig +│ │ ├── tool_result.rs # ToolResult +│ │ ├── react_agent.rs # create_react_agent() +│ │ ├── task_registry.rs # TaskRegistry +│ │ ├── thinking.rs # Thinking graph (custom) +│ │ ├── mcp_tool.rs # MCP tool integration (custom) +│ │ ├── lance_storage.rs # LanceSessionStorage +│ │ ├── storage_postgres.rs # PostgresSessionStorage +│ │ └── agents/ +│ │ ├── agent_card.rs # AgentCard YAML +│ │ └── langgraph_import.rs # JSON import +│ └── Cargo.toml +│ +├── graph-flow-server/ # HTTP API (≈ langgraph-api) +│ ├── src/lib.rs +│ └── Cargo.toml +│ +├── examples/ # Example applications +├── insurance-claims-service/ # Full example app +├── recommendation-service/ # Full example app +└── medical-document-service/ # Full example app +``` + +--- + +## Recommended Rust Crate Layout (Target) + +``` +rs-graph-llm/ +├── graph-flow/ # Core engine (≈ langgraph core) +│ ├── src/ +│ │ ├── lib.rs +│ │ │ +│ │ ├── # ─── Graph Construction ─── +│ │ ├── graph.rs # Graph, GraphBuilder, Edge, ConditionalEdgeSet +│ │ ├── compat.rs # StateGraph, START/END, Command, RoutingDecision +│ │ │ +│ │ ├── # ─── Task System ─── +│ │ ├── task.rs # Task trait, NextAction, TaskResult +│ │ ├── subgraph.rs # SubgraphTask +│ │ ├── fanout.rs # FanOutTask (≈ Send) +│ │ │ +│ │ ├── # ─── State Management ─── +│ │ ├── context.rs # Context, ChatHistory, MessageRole +│ │ ├── typed_context.rs # TypedContext, State trait +│ │ ├── channels.rs # Channels, ChannelReducer, ChannelConfig +│ │ │ +│ │ ├── # ─── Execution Engine ─── +│ │ ├── runner.rs # FlowRunner (run, run_batch, run_with_config) +│ │ ├── streaming.rs # StreamingRunner, StreamChunk, StreamMode +│ │ ├── run_config.rs # RunConfig, BreakpointConfig +│ │ ├── retry.rs # RetryPolicy, BackoffStrategy +│ │ │ +│ │ ├── # ─── Storage / Checkpointing ─── +│ │ ├── storage.rs # Session, SessionStorage, InMemorySessionStorage +│ │ ├── storage_postgres.rs # PostgresSessionStorage +│ │ ├── lance_storage.rs # LanceSessionStorage (time-travel) +│ │ │ +│ │ ├── # ─── Store (NEW — Long-term Memory) ─── +│ │ ├── store/ # NEW module +│ │ │ ├── mod.rs # BaseStore trait, Item, SearchItem +│ │ │ ├── memory.rs # InMemoryStore +│ │ │ └── lance.rs # LanceStore (backed by lance-graph) +│ │ │ +│ │ ├── # ─── Errors ─── +│ │ ├── error.rs # GraphError, Result +│ │ │ +│ │ ├── # ─── Prebuilt Components ─── +│ │ ├── prebuilt/ # NEW submodule (was flat files) +│ │ │ ├── mod.rs +│ │ │ ├── react_agent.rs # create_react_agent() (moved from react_agent.rs) +│ │ │ ├── tool_node.rs # ToolNode, tools_condition (NEW) +│ │ │ ├── interrupt.rs # HumanInterrupt types (NEW) +│ │ │ └── validation.rs # ValidationNode (NEW) +│ │ │ +│ │ ├── # ─── Tool System ─── +│ │ ├── tool_result.rs # ToolResult +│ │ ├── mcp_tool.rs # McpToolTask, MockMcpToolTask +│ │ │ +│ │ ├── # ─── Agent Cards ─── +│ │ ├── agents/ +│ │ │ ├── agent_card.rs # AgentCard, compile_agent_card +│ │ │ └── langgraph_import.rs # LangGraph JSON import +│ │ ├── task_registry.rs # TaskRegistry +│ │ │ +│ │ └── # ─── Custom (Our Additions) ─── +│ │ └── thinking.rs # Thinking graph +│ │ +│ └── Cargo.toml +│ +├── graph-flow-server/ # HTTP API server (≈ langgraph-api) +│ ├── src/ +│ │ ├── lib.rs # Router, endpoints +│ │ ├── sse.rs # SSE streaming endpoint (NEW) +│ │ └── middleware.rs # Auth, CORS, logging (NEW) +│ └── Cargo.toml +│ +├── graph-flow-macros/ # Proc macros (NEW — ≈ langgraph.func) +│ ├── src/lib.rs # #[task], #[entrypoint] macros +│ └── Cargo.toml +│ +├── examples/ +├── insurance-claims-service/ +├── recommendation-service/ +└── medical-document-service/ +``` + +--- + +## Mapping: Python Package → Rust Crate + +| Python Package | Rust Crate | Status | +|---------------|-----------|--------| +| `langgraph` (core) | `graph-flow` | EXISTS | +| `langgraph-prebuilt` | `graph-flow` (prebuilt/ submodule) | PARTIAL | +| `langgraph-checkpoint` (base) | `graph-flow` (storage.rs) | EXISTS | +| `langgraph-checkpoint-postgres` | `graph-flow` (storage_postgres.rs, feature-gated) | EXISTS | +| `langgraph-checkpoint-sqlite` | Not planned (Postgres + Lance sufficient) | SKIP | +| `langgraph.store` | `graph-flow` (store/ submodule) | NEW | +| `langgraph-api` / `langgraph-cli` | `graph-flow-server` | EXISTS | +| `langgraph.func` (@task/@entrypoint) | `graph-flow-macros` | NEW | + +--- + +## Mapping: Python Module → Rust Module + +| Python Module | Rust Module | File | +|--------------|------------|------| +| `langgraph.graph.state.StateGraph` | `compat::StateGraph` | `compat.rs` | +| `langgraph.graph.state.CompiledStateGraph` | `graph::Graph` | `graph.rs` | +| `langgraph.graph.message` | `context::ChatHistory` | `context.rs` | +| `langgraph.graph._branch` | `graph::ConditionalEdgeSet` | `graph.rs` | +| `langgraph.pregel.main.Pregel` | `graph::Graph` + `runner::FlowRunner` | `graph.rs` + `runner.rs` | +| `langgraph.pregel.main.NodeBuilder` | `graph::GraphBuilder` | `graph.rs` | +| `langgraph.pregel.protocol` | `task::Task` trait | `task.rs` | +| `langgraph.pregel.remote.RemoteGraph` | `graph-flow-server` (server side) | `lib.rs` | +| `langgraph.channels.*` | `channels::*` | `channels.rs` | +| `langgraph.types.Command` | `compat::Command` | `compat.rs` | +| `langgraph.types.Send` | `fanout::FanOutTask` | `fanout.rs` | +| `langgraph.types.RetryPolicy` | `retry::RetryPolicy` | `retry.rs` | +| `langgraph.types.StreamMode` | `streaming::StreamMode` | `streaming.rs` | +| `langgraph.types.Interrupt` | `task::NextAction::WaitForInput` | `task.rs` | +| `langgraph.errors.*` | `error::GraphError` | `error.rs` | +| `langgraph.checkpoint.base` | `storage::SessionStorage` | `storage.rs` | +| `langgraph.checkpoint.memory` | `storage::InMemorySessionStorage` | `storage.rs` | +| `langgraph.checkpoint.postgres` | `storage_postgres::PostgresSessionStorage` | `storage_postgres.rs` | +| `langgraph.store.base` | `store::BaseStore` (NEW) | `store/mod.rs` | +| `langgraph.store.memory` | `store::InMemoryStore` (NEW) | `store/memory.rs` | +| `langgraph.prebuilt.chat_agent_executor` | `prebuilt::react_agent` | `react_agent.rs` | +| `langgraph.prebuilt.tool_node` | `prebuilt::tool_node` (NEW) | `prebuilt/tool_node.rs` | +| `langgraph.prebuilt.interrupt` | `prebuilt::interrupt` (NEW) | `prebuilt/interrupt.rs` | +| `langgraph.func.task` | `graph-flow-macros` `#[task]` (NEW) | `macros/src/lib.rs` | +| `langgraph.func.entrypoint` | `graph-flow-macros` `#[entrypoint]` (NEW) | `macros/src/lib.rs` | +| `langgraph.runtime` | TBD | — | + +--- + +## Feature Flags + +```toml +[features] +default = ["memory"] +memory = [] # InMemorySessionStorage, InMemoryStore +postgres = ["sqlx"] # PostgresSessionStorage +lance = ["lance"] # LanceSessionStorage, LanceStore +mcp = ["rmcp"] # McpToolTask +rig = ["rig-core"] # Rig LLM integration +macros = ["graph-flow-macros"] # #[task], #[entrypoint] proc macros +server = ["axum", "tower"] # HTTP server components +``` + +--- + +## Key Architectural Differences from Python + +1. **No Runnable abstraction**: Python LangGraph builds on LangChain's `Runnable` protocol. Rust uses the `Task` trait directly — simpler and more performant. + +2. **No channel-per-field model**: Python LangGraph creates one channel per state field. Rust uses `Context` (DashMap) for most cases, with explicit `Channels` for reducer semantics when needed. + +3. **No Pregel superstep model**: Python uses a Pregel-inspired superstep execution where all triggered nodes run per step. Rust uses sequential task execution with explicit edges — simpler but less parallel. + +4. **Session = Checkpoint**: Python separates Checkpoint (state) from Thread (session). Rust combines them into `Session` with context + position. + +5. **Async-native**: Python has sync + async variants for everything. Rust is async-native — no sync wrappers needed (use `tokio::runtime::Runtime::block_on()` for sync callers). + +6. **Storage integration**: Python has separate checkpoint + store packages. Rust integrates both into `graph-flow` with feature flags, and lance-graph provides the vector search backend directly. diff --git a/.claude/LANGGRAPH_FULL_INVENTORY.md b/.claude/LANGGRAPH_FULL_INVENTORY.md new file mode 100644 index 0000000..3145734 --- /dev/null +++ b/.claude/LANGGRAPH_FULL_INVENTORY.md @@ -0,0 +1,376 @@ +# LangGraph Full Inventory: Python → Rust Mapping + +> Every public class, method, constant, and type in Python LangGraph mapped to its Rust equivalent (or marked MISSING). + +--- + +## 1. `langgraph.constants` + +| Python | Rust Equivalent | Location | +|--------|----------------|----------| +| `START = "__start__"` | `pub const START: &str = "__start__"` | `graph-flow/src/compat.rs` | +| `END = "__end__"` | `pub const END: &str = "__end__"` | `graph-flow/src/compat.rs` | +| `TAG_NOSTREAM` | MISSING | — | +| `TAG_HIDDEN` | MISSING | — | + +--- + +## 2. `langgraph.types` + +| Python | Rust Equivalent | Location | +|--------|----------------|----------| +| `StreamMode` (Literal: values/updates/debug/messages/custom/events) | `pub enum StreamMode { Values, Updates, Debug }` | `graph-flow/src/streaming.rs` | +| `StreamWriter` (Callable) | `mpsc::Sender` | `graph-flow/src/streaming.rs` | +| `RetryPolicy` (NamedTuple) | `pub struct RetryPolicy` | `graph-flow/src/retry.rs` | +| `CachePolicy` | MISSING | — | +| `Interrupt` | MISSING (partial via `NextAction::WaitForInput`) | — | +| `StateSnapshot` (NamedTuple) | MISSING | — | +| `Send` (fanout dispatch) | `pub struct FanOutTask` | `graph-flow/src/fanout.rs` | +| `Command` (Generic) | `pub enum Command` | `graph-flow/src/compat.rs` | +| `Command.goto()` | `Command::goto()` | `graph-flow/src/compat.rs` | +| `Command.resume()` | `Command::resume()` | `graph-flow/src/compat.rs` | +| `Command.update()` | `Command::update()` | `graph-flow/src/compat.rs` | +| `interrupt()` function | MISSING | — | +| `Overwrite` | MISSING | — | +| `PregelTask` | MISSING (internal) | — | +| `PregelExecutableTask` | MISSING (internal) | — | +| `StateUpdate` | MISSING | — | +| `CacheKey` | MISSING | — | +| `CheckpointTask` | MISSING | — | +| `TaskPayload` | MISSING | — | +| `TaskResultPayload` | MISSING | — | +| `CheckpointPayload` | MISSING | — | +| `ValuesStreamPart` | `StreamChunk` (partial) | `graph-flow/src/streaming.rs` | +| `UpdatesStreamPart` | `StreamChunk` (partial) | `graph-flow/src/streaming.rs` | +| `MessagesStreamPart` | `StreamChunk` (partial) | `graph-flow/src/streaming.rs` | +| `CustomStreamPart` | MISSING | — | +| `DebugStreamPart` | `StreamChunk` (partial) | `graph-flow/src/streaming.rs` | +| `GraphOutput` | `ExecutionResult` | `graph-flow/src/graph.rs` | +| `RoutingDecision` (not in Python) | `pub enum RoutingDecision` | `graph-flow/src/compat.rs` | + +--- + +## 3. `langgraph.errors` + +| Python | Rust Equivalent | Location | +|--------|----------------|----------| +| `GraphRecursionError` | `RunConfig.recursion_limit` (guarded) | `graph-flow/src/run_config.rs` | +| `InvalidUpdateError` | `GraphError::TaskExecutionFailed` | `graph-flow/src/error.rs` | +| `GraphBubbleUp` | MISSING | — | +| `GraphInterrupt` | `NextAction::WaitForInput` (partial) | `graph-flow/src/task.rs` | +| `NodeInterrupt` | MISSING | — | +| `ParentCommand` | MISSING | — | +| `EmptyInputError` | MISSING | — | +| `TaskNotFound` (exception) | `GraphError::TaskNotFound` | `graph-flow/src/error.rs` | +| `EmptyChannelError` | MISSING | — | +| `ErrorCode` (enum) | MISSING | — | + +--- + +## 4. `langgraph.config` + +| Python | Rust Equivalent | Location | +|--------|----------------|----------| +| `get_config()` | MISSING (no LangChain RunnableConfig) | — | +| `get_store()` | MISSING | — | +| `get_stream_writer()` | MISSING | — | + +--- + +## 5. `langgraph.graph.state` — `StateGraph` + +| Python Method | Rust Equivalent | Location | +|---------------|----------------|----------| +| `StateGraph.__init__(state_schema)` | `StateGraph::new(name)` | `graph-flow/src/compat.rs` | +| `StateGraph.add_node(name, fn)` | `StateGraph::add_node(name, task)` | `graph-flow/src/compat.rs` | +| `StateGraph.add_edge(from, to)` | `StateGraph::add_edge(from, to)` | `graph-flow/src/compat.rs` | +| `StateGraph.add_conditional_edges(source, path, path_map)` | `StateGraph::add_conditional_edges(source, cond, yes, no)` | `graph-flow/src/compat.rs` | +| `StateGraph.add_sequence(nodes)` | MISSING | — | +| `StateGraph.set_entry_point(key)` | `StateGraph::set_entry_point(node)` | `graph-flow/src/compat.rs` | +| `StateGraph.set_conditional_entry_point(path, path_map)` | MISSING | — | +| `StateGraph.set_finish_point(key)` | MISSING (implicit via END) | — | +| `StateGraph.compile(checkpointer, interrupt_before, interrupt_after)` | `StateGraph::compile()` (no checkpointer arg) | `graph-flow/src/compat.rs` | +| `StateGraph.validate()` | MISSING | — | +| `CompiledStateGraph` (subclass of Pregel) | `Graph` | `graph-flow/src/graph.rs` | +| `CompiledStateGraph.get_input_jsonschema()` | MISSING | — | +| `CompiledStateGraph.get_output_jsonschema()` | MISSING | — | +| `CompiledStateGraph.attach_node()` | MISSING (internal) | — | +| `CompiledStateGraph.attach_edge()` | MISSING (internal) | — | +| `CompiledStateGraph.attach_branch()` | MISSING (internal) | — | + +--- + +## 6. `langgraph.graph.message` + +| Python | Rust Equivalent | Location | +|--------|----------------|----------| +| `add_messages()` reducer | `Context::add_user_message()` / `add_assistant_message()` | `graph-flow/src/context.rs` | +| `MessageGraph` | MISSING (use StateGraph with ChatHistory) | — | +| `MessagesState` (TypedDict) | `ChatHistory` | `graph-flow/src/context.rs` | +| `push_message()` | `Context::add_user_message()` | `graph-flow/src/context.rs` | + +--- + +## 7. `langgraph.graph._branch` + +| Python | Rust Equivalent | Location | +|--------|----------------|----------| +| `BranchSpec` | `ConditionalEdgeSet` / binary conditional edges | `graph-flow/src/graph.rs` | +| `BranchSpec.run()` | `Graph::find_next_task()` | `graph-flow/src/graph.rs` | + +--- + +## 8. `langgraph.pregel.main` — `Pregel` (Core Engine) + +| Python Method | Rust Equivalent | Location | +|---------------|----------------|----------| +| `Pregel.__init__(nodes, channels, ...)` | `GraphBuilder::new().build()` | `graph-flow/src/graph.rs` | +| `Pregel.invoke(input, config)` | `Graph::execute_session()` | `graph-flow/src/graph.rs` | +| `Pregel.stream(input, config, stream_mode)` | `StreamingRunner::stream()` | `graph-flow/src/streaming.rs` | +| `Pregel.astream(input, config, stream_mode)` | `StreamingRunner::stream()` (async native) | `graph-flow/src/streaming.rs` | +| `Pregel.ainvoke(input, config)` | `Graph::execute_session()` (async native) | `graph-flow/src/graph.rs` | +| `Pregel.get_state(config)` | `SessionStorage::get()` | `graph-flow/src/storage.rs` | +| `Pregel.aget_state(config)` | `SessionStorage::get()` (async native) | `graph-flow/src/storage.rs` | +| `Pregel.get_state_history(config)` | `LanceSessionStorage::list_versions()` | `graph-flow/src/lance_storage.rs` | +| `Pregel.update_state(config, values)` | `Context::set()` + `SessionStorage::save()` | `graph-flow/src/context.rs` | +| `Pregel.bulk_update_state(configs, values)` | MISSING | — | +| `Pregel.get_graph()` | MISSING (no Mermaid export) | — | +| `Pregel.get_subgraphs()` | MISSING | — | +| `Pregel.clear_cache(nodes)` | MISSING | — | +| `Pregel.with_config(config)` | `FlowRunner::run_with_config()` | `graph-flow/src/runner.rs` | +| `Pregel.copy()` | MISSING | — | +| `Pregel.config_schema()` | MISSING | — | +| `Pregel.validate()` | MISSING | — | +| `NodeBuilder` | `GraphBuilder` | `graph-flow/src/graph.rs` | +| `NodeBuilder.subscribe_to()` | MISSING (uses edge model) | — | +| `NodeBuilder.write_to()` | MISSING (uses edge model) | — | +| `NodeBuilder.build()` | `GraphBuilder::build()` | `graph-flow/src/graph.rs` | + +--- + +## 9. `langgraph.pregel.protocol` — `PregelProtocol` + +| Python | Rust Equivalent | Location | +|--------|----------------|----------| +| `PregelProtocol` (abstract base) | `trait Task` + `Graph` | `graph-flow/src/task.rs` + `graph.rs` | +| `StreamProtocol` | `StreamingRunner` | `graph-flow/src/streaming.rs` | + +--- + +## 10. `langgraph.pregel.remote` — `RemoteGraph` + +| Python | Rust Equivalent | Location | +|--------|----------------|----------| +| `RemoteGraph` | `graph-flow-server` (HTTP client not impl) | `graph-flow-server/src/lib.rs` | +| `RemoteException` | MISSING | — | + +--- + +## 11. `langgraph.pregel.types` + +| Python | Rust Equivalent | Location | +|--------|----------------|----------| +| `PregelTask` | MISSING (internal) | — | +| `PregelExecutableTask` | MISSING (internal) | — | + +--- + +## 12. `langgraph.channels` — Channel System + +| Python Channel | Rust Equivalent | Location | +|---------------|----------------|----------| +| `BaseChannel` (ABC) | `pub enum ChannelReducer` | `graph-flow/src/channels.rs` | +| `BaseChannel.update()` | `Channels::apply()` | `graph-flow/src/channels.rs` | +| `BaseChannel.get()` | `Channels::get()` | `graph-flow/src/channels.rs` | +| `BaseChannel.checkpoint()` | `Channels::snapshot()` | `graph-flow/src/channels.rs` | +| `BaseChannel.from_checkpoint()` | MISSING | — | +| `BaseChannel.is_available()` | MISSING | — | +| `BaseChannel.consume()` | MISSING | — | +| `BaseChannel.finish()` | MISSING | — | +| `LastValue` | `ChannelReducer::LastValue` | `graph-flow/src/channels.rs` | +| `LastValueAfterFinish` | MISSING | — | +| `AnyValue` | MISSING | — | +| `BinaryOperatorAggregate` | `ChannelReducer::Custom(ReducerFn)` | `graph-flow/src/channels.rs` | +| `Topic` (accumulate list) | `ChannelReducer::Append` | `graph-flow/src/channels.rs` | +| `EphemeralValue` | MISSING | — | +| `NamedBarrierValue` | MISSING | — | +| `NamedBarrierValueAfterFinish` | MISSING | — | +| `UntrackedValue` | MISSING | — | + +--- + +## 13. `langgraph.managed` — Managed Values + +| Python | Rust Equivalent | Location | +|--------|----------------|----------| +| `ManagedValue` (ABC) | MISSING | — | +| `IsLastStepManager` | MISSING | — | +| `RemainingStepsManager` | MISSING | — | +| `is_managed_value()` | MISSING | — | + +--- + +## 14. `langgraph.func` — Functional API + +| Python | Rust Equivalent | Location | +|--------|----------------|----------| +| `@task` decorator | MISSING (use `impl Task` trait) | — | +| `@entrypoint` decorator | MISSING (use `GraphBuilder`) | — | +| `_TaskFunction` | MISSING | — | +| `SyncAsyncFuture` | MISSING (Rust async native) | — | + +--- + +## 15. `langgraph.runtime` + +| Python | Rust Equivalent | Location | +|--------|----------------|----------| +| `Runtime` class | MISSING | — | +| `get_runtime()` | MISSING | — | +| `Runtime.merge()` | MISSING | — | +| `Runtime.override()` | MISSING | — | + +--- + +## 16. `langgraph.prebuilt` (separate package) + +### 16a. `chat_agent_executor` + +| Python | Rust Equivalent | Location | +|--------|----------------|----------| +| `create_react_agent(model, tools, ...)` | `create_react_agent(llm_task, tools, max_iter)` | `graph-flow/src/react_agent.rs` | +| `AgentState` (TypedDict) | Context keys: `needs_tool`, `selected_tool`, etc. | `graph-flow/src/react_agent.rs` | +| `AgentStatePydantic` | MISSING | — | +| `call_model()` / `acall_model()` | Internal `IterationGuardTask` | `graph-flow/src/react_agent.rs` | +| `should_continue()` | Conditional edge on `needs_tool` key | `graph-flow/src/react_agent.rs` | +| `generate_structured_response()` | MISSING | — | +| `route_tool_responses()` | MISSING | — | +| Prompt/system message support | MISSING | — | +| Model selection via Runtime | MISSING | — | +| `post_model_hook` | MISSING | — | + +### 16b. `tool_node` + +| Python | Rust Equivalent | Location | +|--------|----------------|----------| +| `ToolNode` | `ToolRouterTask` + `ToolAggregatorTask` | `graph-flow/src/react_agent.rs` | +| `tools_condition()` | Conditional edge in `create_react_agent` | `graph-flow/src/react_agent.rs` | +| `ToolCallRequest` | MISSING | — | +| `InjectedState` | MISSING | — | +| `InjectedStore` | MISSING | — | +| `ToolRuntime` | MISSING | — | +| `msg_content_output()` | MISSING | — | +| `ToolInvocationError` | `ToolResult::Error` | `graph-flow/src/tool_result.rs` | +| `ValidationNode` | MISSING | — | + +### 16c. `interrupt` + +| Python | Rust Equivalent | Location | +|--------|----------------|----------| +| `HumanInterruptConfig` | MISSING | — | +| `HumanInterrupt` | `NextAction::WaitForInput` (basic) | `graph-flow/src/task.rs` | +| `HumanResponse` | MISSING | — | +| `ActionRequest` | MISSING | — | + +--- + +## 17. `langgraph.checkpoint` (separate package) + +| Python | Rust Equivalent | Location | +|--------|----------------|----------| +| `BaseCheckpointSaver` | `trait SessionStorage` | `graph-flow/src/storage.rs` | +| `BaseCheckpointSaver.get()` | `SessionStorage::get()` | `graph-flow/src/storage.rs` | +| `BaseCheckpointSaver.put()` | `SessionStorage::save()` | `graph-flow/src/storage.rs` | +| `BaseCheckpointSaver.put_writes()` | MISSING | — | +| `BaseCheckpointSaver.list()` | `LanceSessionStorage::list_versions()` | `graph-flow/src/lance_storage.rs` | +| `BaseCheckpointSaver.delete_thread()` | `SessionStorage::delete()` | `graph-flow/src/storage.rs` | +| `BaseCheckpointSaver.copy_thread()` | MISSING | — | +| `BaseCheckpointSaver.prune()` | MISSING | — | +| `BaseCheckpointSaver.get_next_version()` | MISSING | — | +| `Checkpoint` (TypedDict) | `Session` | `graph-flow/src/storage.rs` | +| `CheckpointMetadata` | MISSING | — | +| `CheckpointTuple` | `VersionedSession` | `graph-flow/src/lance_storage.rs` | +| `copy_checkpoint()` | MISSING | — | +| `empty_checkpoint()` | `Session::new_from_task()` | `graph-flow/src/storage.rs` | +| `MemorySaver` | `InMemorySessionStorage` | `graph-flow/src/storage.rs` | +| `PostgresSaver` | `PostgresSessionStorage` | `graph-flow/src/storage_postgres.rs` | +| `SqliteSaver` | MISSING | — | +| Serde (jsonplus/msgpack) | `serde_json` (JSON only) | — | +| `EncryptedSerde` | MISSING | — | + +--- + +## 18. `langgraph.store` (separate package) + +| Python | Rust Equivalent | Location | +|--------|----------------|----------| +| `BaseStore` (ABC) | MISSING | — | +| `BaseStore.get()` | MISSING | — | +| `BaseStore.search()` | MISSING | — | +| `BaseStore.put()` | MISSING | — | +| `BaseStore.delete()` | MISSING | — | +| `BaseStore.list_namespaces()` | MISSING | — | +| `Item` | MISSING | — | +| `SearchItem` | MISSING | — | +| `GetOp` / `SearchOp` / `PutOp` | MISSING | — | +| `ListNamespacesOp` | MISSING | — | +| `MatchCondition` | MISSING | — | +| `AsyncBatchedBaseStore` | MISSING | — | +| `InMemoryStore` | MISSING | — | +| `PostgresStore` | MISSING | — | +| `IndexConfig` / `TTLConfig` | MISSING | — | +| Embeddings support | MISSING (lance-graph has vector search) | — | + +--- + +## 19. `langgraph._internal` (private but important) + +| Python | Rust Equivalent | Location | +|--------|----------------|----------| +| `_retry.default_retry_on()` | `RetryPolicy` | `graph-flow/src/retry.rs` | +| `_config.merge_configs()` | MISSING | — | +| `_config.patch_config()` | MISSING | — | +| `_config.ensure_config()` | MISSING | — | +| `_serde.*` (serialization allowlist) | MISSING (not needed in Rust) | — | +| `_cache.*` | MISSING | — | +| `_replay.*` | MISSING | — | + +--- + +## 20. `langgraph.pregel` (execution internals) + +| Python | Rust Equivalent | Location | +|--------|----------------|----------| +| `_algo.py` (superstep algorithm) | `Graph::execute_session()` loop | `graph-flow/src/graph.rs` | +| `_loop.py` (execution loop) | `FlowRunner::run()` | `graph-flow/src/runner.rs` | +| `_io.py` (input/output mapping) | `SubgraphTask` mappings | `graph-flow/src/subgraph.rs` | +| `_checkpoint.py` | `SessionStorage::save()/get()` | `graph-flow/src/storage.rs` | +| `_retry.py` (retry logic) | `RetryPolicy` | `graph-flow/src/retry.rs` | +| `_read.py` (channel reads) | `Context::get()` | `graph-flow/src/context.rs` | +| `_write.py` (channel writes) | `Context::set()` | `graph-flow/src/context.rs` | +| `_validate.py` | MISSING | — | +| `_draw.py` (Mermaid diagrams) | MISSING | — | +| `_messages.py` (message handling) | `ChatHistory` | `graph-flow/src/context.rs` | +| `_executor.py` (task execution) | `Graph::execute()` | `graph-flow/src/graph.rs` | +| `_runner.py` (step runner) | `FlowRunner` | `graph-flow/src/runner.rs` | +| `debug.py` (debug utilities) | `StreamMode::Debug` | `graph-flow/src/streaming.rs` | + +--- + +## Summary Statistics + +| Category | Python Count | Rust Implemented | Rust Missing | Coverage | +|----------|-------------|-----------------|-------------|----------| +| Constants | 4 | 2 | 2 | 50% | +| Core Types | 20 | 8 | 12 | 40% | +| Errors | 10 | 3 | 7 | 30% | +| StateGraph API | 12 | 7 | 5 | 58% | +| Pregel/Engine | 20 | 10 | 10 | 50% | +| Channels | 10 | 4 | 6 | 40% | +| Managed Values | 4 | 0 | 4 | 0% | +| Functional API | 3 | 0 | 3 | 0% | +| Runtime | 4 | 0 | 4 | 0% | +| Prebuilt/ReAct | 15 | 4 | 11 | 27% | +| Checkpoint | 15 | 8 | 7 | 53% | +| Store | 15 | 0 | 15 | 0% | +| **TOTAL** | **132** | **46** | **86** | **35%** | diff --git a/.claude/LANGGRAPH_OUR_ADDITIONS.md b/.claude/LANGGRAPH_OUR_ADDITIONS.md new file mode 100644 index 0000000..4536f37 --- /dev/null +++ b/.claude/LANGGRAPH_OUR_ADDITIONS.md @@ -0,0 +1,304 @@ +# Our Additions: Features in Rust graph-flow That LangGraph Doesn't Have + +> Things we built that go beyond Python LangGraph's feature set. + +--- + +## 1. Lance-backed Session Storage with Time Travel + +**File:** `graph-flow/src/lance_storage.rs` + +Python LangGraph has checkpoint versioning via `get_state_history()`, but it relies on generic checkpoint savers (Postgres, SQLite). Our `LanceSessionStorage` uses the Lance columnar format for: + +- **Efficient versioned storage** — each save creates a new version +- **Time travel** — `get_at_version(session_id, version)` to retrieve any historical state +- **Checkpoint namespacing** — `save_namespaced()` / `get_namespaced()` for multi-tenant isolation +- **Columnar scan performance** — Lance's zero-copy reads for session enumeration +- **Native vector search integration** — can leverage lance-graph's vector indices for semantic session retrieval + +```rust +let storage = LanceSessionStorage::new("/data/sessions"); +storage.save(session).await?; + +// Time travel +let versions = storage.list_versions("thread_1").await?; +let v2_session = storage.get_at_version("thread_1", 2).await?; + +// Namespaced +storage.save_namespaced(session, "tenant_a").await?; +let s = storage.get_namespaced("thread_1", "tenant_a").await?; +``` + +--- + +## 2. Agent Card YAML System + +**Files:** `graph-flow/src/agents/agent_card.rs`, `graph-flow/src/task_registry.rs` + +Python LangGraph has no declarative agent definition format. We provide: + +- **YAML-based agent cards** — define agents, capabilities, tools, and workflows declaratively +- **TaskRegistry** — bind real task implementations to capability names +- **Capability placeholders** — `CapabilityTask` stubs for capabilities without implementations +- **Workflow compilation** — YAML → executable `Graph` with real tasks + +```yaml +agent: + name: research_agent + description: Searches and summarizes + capabilities: + - search + - summarize + tools: + - name: web_search + mcp_server: "http://localhost:8080" + workflow: + - task: search + next: summarize + - task: summarize + next: end +``` + +```rust +let mut registry = TaskRegistry::new(); +registry.register("search", Arc::new(SearchTask)); +registry.register("summarize", Arc::new(SummarizeTask)); +let graph = registry.compile_agent_card(yaml)?; +``` + +--- + +## 3. LangGraph JSON Import + +**File:** `graph-flow/src/agents/langgraph_import.rs` + +Direct import of LangGraph workflow definitions from JSON: + +```rust +let def = LangGraphDef { + name: "my_flow".to_string(), + nodes: vec![...], + edges: vec![...], + entry_point: Some("start".to_string()), +}; +let graph = import_langgraph_workflow(&def)?; +``` + +This enables Python → Rust migration by exporting LangGraph definitions as JSON and importing into Rust. + +--- + +## 4. MCP Tool Integration + +**File:** `graph-flow/src/mcp_tool.rs` + +Native Model Context Protocol (MCP) integration as tasks: + +- **`McpToolTask`** — connects to MCP servers and invokes tools +- **`MockMcpToolTask`** — mock for testing without real MCP servers +- **Configurable** — `McpToolConfig` with server URL, tool name, input/output keys, static params, timeout + +Python LangGraph relies on LangChain's tool abstraction. Our MCP integration is direct and protocol-native. + +```rust +let tool = McpToolTask::new("search", "http://mcp-server:8080", "web_search"); +// or with full config +let config = McpToolConfig::new("http://mcp-server:8080", "web_search"); +let tool = McpToolTask::with_config("search", config); +``` + +--- + +## 5. Thinking Graph + +**File:** `graph-flow/src/thinking.rs` + +A prebuilt graph for structured "thinking" workflows — not present in Python LangGraph: + +```rust +let thinking_graph = build_thinking_graph(); +``` + +Provides a chain-of-thought reasoning structure as a ready-to-use graph. + +--- + +## 6. GoBack Navigation + +**Files:** `graph-flow/src/task.rs`, `graph-flow/src/storage.rs` + +Python LangGraph has no concept of "going back" in a workflow. Our system provides: + +- **`NextAction::GoBack`** — a task can navigate back to its previous task +- **`Session::task_history`** — stack of visited tasks +- **`Session::advance_to()`** — push current to history, move forward +- **`Session::go_back()`** — pop history, return to previous + +This enables interactive workflows where users can undo/revisit steps. + +```rust +// In a task's run() method: +Ok(TaskResult::new(Some("Going back".to_string()), NextAction::GoBack)) + +// Session tracks history automatically +session.advance_to("next_task".to_string()); +let prev = session.go_back(); // Returns previous task ID +``` + +--- + +## 7. `ContinueAndExecute` Flow Control + +**File:** `graph-flow/src/task.rs` + +Python LangGraph always returns control to the caller between steps (or runs to completion). We have a third option: + +- **`NextAction::Continue`** — move to next task, return control to caller (step-by-step) +- **`NextAction::ContinueAndExecute`** — move to next task AND execute it immediately (fire-and-forget chaining) +- **`NextAction::GoTo(task_id)`** — jump to specific task + +`ContinueAndExecute` enables task chains that execute without returning intermediate results. + +--- + +## 8. Rig LLM Integration + +**File:** `graph-flow/src/context.rs` (feature-gated) + +Direct integration with the [Rig](https://github.com/0xPlaygrounds/rig) Rust LLM framework: + +- **`Context::get_rig_messages()`** — convert chat history to Rig `Message` format +- **`Context::get_last_rig_messages(n)`** — get last N messages in Rig format + +This provides zero-friction LLM calling from within tasks. + +```rust +#[cfg(feature = "rig")] +async fn run(&self, ctx: Context) -> Result { + let messages = ctx.get_rig_messages().await; + let response = agent.chat("prompt", messages).await?; + ctx.add_assistant_message(response).await; + Ok(TaskResult::new(Some(response), NextAction::Continue)) +} +``` + +--- + +## 9. FanOut Task (Parallel Execution) + +**File:** `graph-flow/src/fanout.rs` + +While Python LangGraph has `Send` for dispatching to nodes, our `FanOutTask` provides true parallel task execution: + +- Spawns all child tasks concurrently via `tokio::spawn` +- Aggregates results into context with configurable prefixes +- Works as a regular `Task` in the graph + +```rust +let fanout = FanOutTask::new("parallel_search", vec![ + Arc::new(WebSearchTask), + Arc::new(DbSearchTask), + Arc::new(CacheSearchTask), +]); +``` + +--- + +## 10. TypedContext with State Trait + +**File:** `graph-flow/src/typed_context.rs` + +While Python uses `TypedDict` for state schemas, our `TypedContext` provides: + +- **Compile-time type safety** — generic over a `State` trait bound +- **RwLock-protected state** — thread-safe read/write access +- **`update_state()`** — mutable access via closure +- **`snapshot_state()`** — clone current state +- **Dual access** — typed state AND raw Context key-value store + +```rust +#[derive(Clone, Serialize, Deserialize)] +struct MyState { + count: i32, + name: String, +} +impl State for MyState {} + +let typed_ctx = TypedContext::new(MyState { count: 0, name: "init".into() }); +typed_ctx.update_state(|s| s.count += 1); +let snapshot = typed_ctx.snapshot_state(); +``` + +--- + +## 11. ToolResult with Fallback + +**File:** `graph-flow/src/tool_result.rs` + +Python LangGraph has `ToolMessage` for tool responses. Our `ToolResult` adds: + +- **`ToolResult::Fallback { value, reason }`** — graceful degradation with explanation +- **`is_retryable()`** — explicit retry signaling +- **`into_result()`** — convert to standard `Result` + +```rust +let result = ToolResult::fallback( + serde_json::json!({"cached": true}), + "API unavailable, using cached data" +); +``` + +--- + +## 12. Graph-Flow Server (Axum HTTP API) + +**File:** `graph-flow-server/src/lib.rs` + +While Python LangGraph has `langgraph-api` (a separate deployment platform), our server is: + +- **Embeddable** — `create_router()` returns an Axum `Router` you can compose +- **Lightweight** — no separate deployment, just add to your binary +- **LangGraph API compatible** — same REST semantics (threads, runs, state) + +```rust +let app = create_router(graph, storage); +let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?; +axum::serve(listener, app).await?; +``` + +--- + +## 13. lance-graph Integration (Separate Repo) + +**Repo:** `AdaWorldAPI/lance-graph` + +Not part of Python LangGraph at all. Provides: + +- **Graph database** with SPO (Subject-Predicate-Object) triple store +- **BLASGraph** — matrix-based graph operations using BLAS +- **Cypher-like query language** with DataFusion backend +- **Vector search** via Lance native indices +- **HDR fingerprinting** for graph similarity +- **Unity Catalog** integration for enterprise data governance + +This powers the storage layer and enables semantic search capabilities that Python LangGraph achieves via third-party integrations. + +--- + +## Summary + +| Addition | Python LangGraph Has? | Our Advantage | +|----------|----------------------|---------------| +| Lance time-travel storage | No (generic checkpoints) | Columnar + versioned + vector-native | +| Agent Card YAML | No | Declarative agent definitions | +| LangGraph JSON import | No | Python → Rust migration path | +| MCP tool integration | No (uses LangChain tools) | Protocol-native tool calling | +| GoBack navigation | No | Interactive undo/revisit | +| ContinueAndExecute | No (all-or-nothing) | Fine-grained flow control | +| Rig LLM integration | No (uses LangChain) | Rust-native LLM framework | +| FanOut parallel tasks | Partial (Send) | True parallel execution | +| TypedContext | No (TypedDict is runtime) | Compile-time type safety | +| ToolResult::Fallback | No | Graceful degradation | +| Embeddable HTTP server | No (separate platform) | Single-binary deployment | +| lance-graph storage | No | Graph DB + vector search | +| Thinking graph | No | Prebuilt reasoning chain | diff --git a/.claude/LANGGRAPH_PARITY_CHECKLIST.md b/.claude/LANGGRAPH_PARITY_CHECKLIST.md new file mode 100644 index 0000000..76de49a --- /dev/null +++ b/.claude/LANGGRAPH_PARITY_CHECKLIST.md @@ -0,0 +1,223 @@ +# LangGraph Parity Checklist + +> What exists, what's missing, priority ranking for each gap. + +Legend: +- **DONE** = Implemented and tested in Rust +- **PARTIAL** = Exists but incomplete vs Python equivalent +- **MISSING** = Not implemented +- Priority: **P0** (critical), **P1** (important), **P2** (nice-to-have), **P3** (low/skip) + +--- + +## Core Graph Construction + +| Feature | Status | Priority | Notes | +|---------|--------|----------|-------| +| `GraphBuilder` (fluent API) | DONE | — | Fully working | +| `StateGraph` (LangGraph compat) | DONE | — | `compat.rs` | +| `START` / `END` constants | DONE | — | `compat.rs` | +| `add_node()` | DONE | — | Via `add_task()` | +| `add_edge()` | DONE | — | Direct + conditional | +| Binary conditional edges | DONE | — | `add_conditional_edge(from, cond, yes, no)` | +| N-way conditional edges (`path_map`) | DONE | — | `add_conditional_edges(from, path_fn, path_map)` | +| `add_sequence()` (ordered chain) | MISSING | P2 | Sugar — easy to add | +| `set_entry_point()` | DONE | — | `set_start_task()` | +| `set_conditional_entry_point()` | MISSING | P2 | Conditional start routing | +| `set_finish_point()` | MISSING | P3 | Implicit via `NextAction::End` | +| `compile()` | DONE | — | `build()` / `StateGraph::compile()` | +| `validate()` (graph validation) | MISSING | P1 | Detect orphan nodes, cycles, unreachable | + +## Graph Execution + +| Feature | Status | Priority | Notes | +|---------|--------|----------|-------| +| Step-by-step execution | DONE | — | `execute_session()` | +| Run to completion | DONE | — | `FlowRunner::run()` loop | +| Async execution | DONE | — | Native async/await | +| Task timeout | DONE | — | `Graph.task_timeout` | +| Recursion limit | DONE | — | `RunConfig.recursion_limit` | +| Breakpoints (interrupt_before) | DONE | — | `BreakpointConfig` | +| Breakpoints (interrupt_after) | DONE | — | `BreakpointConfig` | +| Dynamic breakpoints | DONE | — | Via `RunConfig` | +| Batch execution | DONE | — | `FlowRunner::run_batch()` | +| `invoke()` equivalent | DONE | — | `execute_session()` | +| `stream()` equivalent | DONE | — | `StreamingRunner::stream()` | +| Stream modes (values/updates/debug) | DONE | — | `StreamMode` enum | +| Stream mode: messages | PARTIAL | P1 | Basic chat history streaming | +| Stream mode: custom | MISSING | P2 | Custom stream channels | +| Stream mode: events | MISSING | P2 | LangSmith-style events | +| `ainvoke()` / `astream()` | DONE | — | All Rust is async-native | +| Tags / metadata on runs | DONE | — | `RunConfig` | + +## State Management + +| Feature | Status | Priority | Notes | +|---------|--------|----------|-------| +| Key-value context | DONE | — | `Context` with DashMap | +| Typed state (`TypedContext`) | DONE | — | Generic state struct | +| Chat history | DONE | — | `ChatHistory` in Context | +| `add_messages` reducer | PARTIAL | P1 | Manual add, no dedup/update by ID | +| Context serialization | DONE | — | `Context::serialize()` | +| Sync + async access | DONE | — | `get_sync()` / `set_sync()` + async | +| State snapshots | MISSING | P1 | `StateSnapshot` equivalent | +| State history / time travel | PARTIAL | P1 | `LanceSessionStorage::list_versions()` | + +## Channels + +| Feature | Status | Priority | Notes | +|---------|--------|----------|-------| +| `LastValue` channel | DONE | — | `ChannelReducer::LastValue` | +| `Topic` (append list) | DONE | — | `ChannelReducer::Append` | +| `BinaryOperatorAggregate` | DONE | — | `ChannelReducer::Custom(fn)` | +| `AnyValue` channel | MISSING | P3 | Rarely used | +| `EphemeralValue` channel | MISSING | P2 | Useful for one-shot data | +| `NamedBarrierValue` | MISSING | P3 | Synchronization primitive | +| `UntrackedValue` | MISSING | P3 | Rarely used | +| Channel checkpoint/restore | MISSING | P2 | From checkpoint support | +| Channel `is_available()` | MISSING | P3 | Availability tracking | +| Channel `consume()` / `finish()` | MISSING | P3 | Lifecycle methods | + +## Checkpointing / Storage + +| Feature | Status | Priority | Notes | +|---------|--------|----------|-------| +| `SessionStorage` trait | DONE | — | `save()` / `get()` / `delete()` | +| In-memory storage | DONE | — | `InMemorySessionStorage` | +| PostgreSQL storage | DONE | — | `PostgresSessionStorage` | +| Lance-backed storage | DONE | — | `LanceSessionStorage` | +| Version history | DONE | — | `list_versions()` / `get_at_version()` | +| Checkpoint namespacing | DONE | — | `save_namespaced()` / `get_namespaced()` | +| `put_writes()` (partial writes) | MISSING | P2 | Write individual channels | +| `copy_thread()` | MISSING | P2 | Clone session state | +| `prune()` (cleanup old) | MISSING | P2 | Storage cleanup | +| `delete_for_runs()` | MISSING | P3 | Selective deletion | +| `CheckpointMetadata` | MISSING | P2 | Rich metadata per checkpoint | +| `get_next_version()` | MISSING | P3 | Auto-version numbering | +| Serde: msgpack | MISSING | P3 | JSON is sufficient | +| Serde: encrypted | MISSING | P2 | Sensitive data at rest | +| SQLite storage | MISSING | P3 | Postgres + Lance cover needs | + +## Subgraphs + +| Feature | Status | Priority | Notes | +|---------|--------|----------|-------| +| `SubgraphTask` | DONE | — | Inner graph execution | +| Shared context | DONE | — | Parent/child share Context | +| Input/output mappings | DONE | — | `with_mappings()` | +| Max iteration guard | DONE | — | 1000 iteration limit | +| `get_subgraphs()` introspection | MISSING | P2 | Enumerate child graphs | + +## Prebuilt Agents + +| Feature | Status | Priority | Notes | +|---------|--------|----------|-------| +| `create_react_agent()` | DONE | — | With iteration guard | +| Tool routing | DONE | — | `ToolRouterTask` | +| Tool aggregation | DONE | — | `ToolAggregatorTask` | +| `ToolNode` (full) | PARTIAL | P1 | Basic routing, no interceptors | +| `tools_condition()` | DONE | — | Conditional edge on `needs_tool` | +| `InjectedState` | MISSING | P1 | Tool state injection | +| `InjectedStore` | MISSING | P2 | Tool store injection | +| `ToolRuntime` | MISSING | P2 | Runtime injection to tools | +| `ToolCallRequest` / interceptors | MISSING | P1 | Request interception pipeline | +| `ValidationNode` | MISSING | P2 | Schema validation for tool calls | +| Prompt / system message | MISSING | P1 | System prompt in ReAct agent | +| Model selection (multi-model) | MISSING | P1 | Dynamic model per-call | +| `generate_structured_response()` | MISSING | P2 | Structured output mode | +| `HumanInterrupt` config | MISSING | P1 | Structured human-in-the-loop | +| `HumanResponse` | MISSING | P1 | Response format | +| `post_model_hook` | MISSING | P2 | Post-inference processing | + +## Agent Cards / YAML + +| Feature | Status | Priority | Notes | +|---------|--------|----------|-------| +| Agent card YAML schema | DONE | — | `AgentCard` struct | +| `compile_agent_card()` | DONE | — | YAML → Graph | +| `TaskRegistry` | DONE | — | Real task bindings | +| Capability placeholders | DONE | — | `CapabilityTask` fallback | +| LangGraph JSON import | DONE | — | `import_langgraph_workflow()` | + +## Error Handling + +| Feature | Status | Priority | Notes | +|---------|--------|----------|-------| +| `GraphError` enum | DONE | — | 7 variants | +| `ToolResult` (success/error/fallback) | DONE | — | Structured results | +| Retry policy | DONE | — | Fixed/Exponential/None | +| `GraphRecursionError` | PARTIAL | P1 | Limit exists, no dedicated error | +| `NodeInterrupt` | MISSING | P1 | Per-node interrupt | +| `GraphInterrupt` | PARTIAL | P1 | Via WaitForInput | +| `InvalidUpdateError` | MISSING | P3 | Via TaskExecutionFailed | +| Error codes | MISSING | P3 | Enum-based codes | + +## Store (Long-term Memory) + +| Feature | Status | Priority | Notes | +|---------|--------|----------|-------| +| `BaseStore` trait | MISSING | P0 | Critical for agent memory | +| `InMemoryStore` | MISSING | P0 | Dev/test store | +| `Item` / `SearchItem` | MISSING | P0 | Store data types | +| `get()` / `put()` / `delete()` | MISSING | P0 | CRUD operations | +| `search()` with embeddings | MISSING | P0 | Vector search (lance-graph!) | +| `list_namespaces()` | MISSING | P1 | Namespace enumeration | +| `MatchCondition` | MISSING | P1 | Filter predicates | +| `IndexConfig` / `TTLConfig` | MISSING | P2 | Index + expiry config | +| `AsyncBatchedBaseStore` | MISSING | P2 | Batched async operations | +| `PostgresStore` | MISSING | P1 | Persistent store | + +## Functional API + +| Feature | Status | Priority | Notes | +|---------|--------|----------|-------| +| `@task` decorator | MISSING | P2 | Macro could work (`#[task]`) | +| `@entrypoint` decorator | MISSING | P2 | Macro for graph entry | +| `SyncAsyncFuture` | N/A | — | Rust is natively async | + +## Runtime + +| Feature | Status | Priority | Notes | +|---------|--------|----------|-------| +| `Runtime` class | MISSING | P2 | Runtime context injection | +| `get_runtime()` | MISSING | P2 | Access current runtime | +| `Runtime.override()` | MISSING | P2 | Override runtime values | + +## HTTP API / Server + +| Feature | Status | Priority | Notes | +|---------|--------|----------|-------| +| Thread creation (POST) | DONE | — | `/threads` | +| Thread execution (POST) | DONE | — | `/threads/{id}/runs` | +| Thread state (GET) | DONE | — | `/threads/{id}/state` | +| Thread deletion (DELETE) | DONE | — | `/threads/{id}` | +| Thread history (GET) | MISSING | P1 | `/threads/{id}/history` | +| SSE streaming endpoint | MISSING | P1 | Server-sent events | +| Cron runs | MISSING | P3 | Scheduled execution | +| Assistants CRUD | MISSING | P2 | Multi-graph management | + +## Visualization / Debugging + +| Feature | Status | Priority | Notes | +|---------|--------|----------|-------| +| Mermaid diagram export | MISSING | P2 | `get_graph()` → Mermaid | +| Debug stream mode | DONE | — | `StreamMode::Debug` | +| Task history tracking | DONE | — | `Session::task_history` | + +--- + +## Priority Summary + +| Priority | Count | Description | +|----------|-------|-------------| +| **P0** | 5 | Store/memory system (critical for agents) | +| **P1** | 18 | Core parity gaps (interrupts, HITL, validation, streaming) | +| **P2** | 22 | Nice-to-have features (functional API, visualization, advanced channels) | +| **P3** | 12 | Low priority (edge cases, rarely used) | + +### Recommended Sprint Order + +1. **Sprint 1 (P0)**: Store/Memory system — `BaseStore` trait, `InMemoryStore`, `Item`/`SearchItem`, integrate with lance-graph vector search +2. **Sprint 2 (P1-core)**: Structured interrupts (`NodeInterrupt`, `HumanInterrupt`/`HumanResponse`), graph validation, SSE streaming +3. **Sprint 3 (P1-agents)**: Enhanced ReAct agent (prompt support, model selection), `InjectedState`, `ToolCallRequest` interceptors +4. **Sprint 4 (P2)**: Functional API macros, Mermaid export, advanced channels, `Runtime` diff --git a/.claude/LANGGRAPH_TRANSCODING_MAP.md b/.claude/LANGGRAPH_TRANSCODING_MAP.md new file mode 100644 index 0000000..85d831b --- /dev/null +++ b/.claude/LANGGRAPH_TRANSCODING_MAP.md @@ -0,0 +1,394 @@ +# LangGraph Transcoding Map: Python → Rust + +> Direct type-for-type, function-for-function mapping between Python LangGraph and Rust graph-flow. + +--- + +## Type Mappings + +| Python Type | Rust Type | Notes | +|------------|----------|-------| +| `str` | `String` / `&str` | | +| `dict[str, Any]` | `serde_json::Map` | Or typed struct | +| `Any` | `serde_json::Value` | Generic storage | +| `list[T]` | `Vec` | | +| `set[T]` | `HashSet` | | +| `tuple[T, ...]` | `(T, ...)` | | +| `Optional[T]` | `Option` | | +| `Callable[[A], R]` | `Arc R + Send + Sync>` | Thread-safe closure | +| `Callable[[A], Awaitable[R]]` | `Arc Pin>> + Send + Sync>` | Async closure | +| `TypedDict` | `#[derive(Serialize, Deserialize)] struct` | | +| `NamedTuple` | `struct` | | +| `Enum` | `enum` | | +| `Literal["a", "b"]` | `enum { A, B }` | | +| `Generic[T]` | `` | | +| `ABC` (abstract base) | `trait` | | +| `Protocol` | `trait` | | +| `TypeVar` | Generic parameter `T` | | +| `TypeAlias` | `type Alias = ...` | | +| `dataclass` | `struct` + derives | | +| `BaseModel` (Pydantic) | `#[derive(Serialize, Deserialize)] struct` | | +| `RunnableConfig` | `RunConfig` | | +| `BaseMessage` | `SerializableMessage` | | +| `ToolCall` | `serde_json::Value` (or typed struct) | | +| `ToolMessage` | `ToolResult` | | + +--- + +## Core Class Mappings + +### StateGraph + +```python +# Python +from langgraph.graph import StateGraph, START, END + +class State(TypedDict): + messages: Annotated[list[BaseMessage], add_messages] + count: int + +graph = StateGraph(State) +graph.add_node("agent", agent_fn) +graph.add_node("tools", tool_fn) +graph.add_edge(START, "agent") +graph.add_conditional_edges("agent", should_continue, {"continue": "tools", "end": END}) +graph.add_edge("tools", "agent") +compiled = graph.compile(checkpointer=MemorySaver()) +``` + +```rust +// Rust +use graph_flow::compat::{StateGraph, START, END}; +use graph_flow::{Task, TaskResult, NextAction, Context}; +use std::sync::Arc; +use std::collections::HashMap; + +let mut sg = StateGraph::new("my_graph"); +sg.add_node("agent", Arc::new(AgentTask)); +sg.add_node("tools", Arc::new(ToolsTask)); +sg.add_edge(START, "agent"); +sg.add_conditional_edges( + "agent", + |ctx: &Context| ctx.get_sync::("route").unwrap_or_default(), + HashMap::from([ + ("continue".to_string(), "tools".to_string()), + ("end".to_string(), END.to_string()), + ]), +); +sg.add_edge("tools", "agent"); +let graph = sg.compile(); +``` + +### GraphBuilder (Rust-native API) + +```rust +use graph_flow::{GraphBuilder, Graph}; + +let graph = GraphBuilder::new("my_graph") + .add_task(Arc::new(AgentTask)) + .add_task(Arc::new(ToolsTask)) + .add_edge("agent", "tools") + .add_conditional_edge("agent", |ctx| ctx.get_sync::("needs_tool").unwrap_or(false), "tools", "done") + .set_start_task("agent") + .build(); +``` + +### Task (Node) + +```python +# Python +def my_node(state: State) -> dict: + return {"messages": [response], "count": state["count"] + 1} + +# Or with config +def my_node(state: State, config: RunnableConfig) -> dict: + return {"messages": [response]} +``` + +```rust +// Rust +use graph_flow::{Task, TaskResult, NextAction, Context}; +use async_trait::async_trait; + +struct MyNode; + +#[async_trait] +impl Task for MyNode { + fn id(&self) -> &str { "my_node" } + + async fn run(&self, ctx: Context) -> graph_flow::Result { + let count: i32 = ctx.get("count").await.unwrap_or(0); + ctx.set("count", count + 1).await; + ctx.add_assistant_message("response".to_string()).await; + Ok(TaskResult::new(Some("done".to_string()), NextAction::Continue)) + } +} +``` + +### Execution + +```python +# Python +result = compiled.invoke({"messages": [("user", "hello")]}) +# or +async for chunk in compiled.astream({"messages": [("user", "hello")]}, stream_mode="updates"): + print(chunk) +``` + +```rust +// Rust — Step by step +let mut session = Session::new_from_task("thread_1".to_string(), "agent"); +session.context.set("input", "hello").await; +let result = graph.execute_session(&mut session).await?; + +// Rust — Run to completion +let runner = FlowRunner::new(Arc::new(graph), storage.clone()); +let result = runner.run("thread_1").await?; + +// Rust — Streaming +let streaming = StreamingRunner::new(Arc::new(graph), storage.clone()); +let mut stream = streaming.stream("thread_1").await?; +while let Some(chunk) = stream.next().await { + println!("{:?}", chunk?); +} +``` + +### Checkpointing + +```python +# Python +from langgraph.checkpoint.memory import MemorySaver +checkpointer = MemorySaver() +compiled = graph.compile(checkpointer=checkpointer) + +# Get state +state = compiled.get_state({"configurable": {"thread_id": "1"}}) + +# Time travel +for state in compiled.get_state_history({"configurable": {"thread_id": "1"}}): + print(state) +``` + +```rust +// Rust +use graph_flow::{InMemorySessionStorage, LanceSessionStorage, SessionStorage}; + +// In-memory +let storage = Arc::new(InMemorySessionStorage::new()); + +// Lance-backed (time-travel) +let storage = Arc::new(LanceSessionStorage::new("/data/sessions")); + +// Get state +let session = storage.get("thread_1").await?.unwrap(); + +// Time travel +let versions = storage.list_versions("thread_1").await?; +let old_session = storage.get_at_version("thread_1", versions[0]).await?; +``` + +### Command + +```python +# Python +from langgraph.types import Command, interrupt + +def my_node(state): + answer = interrupt("What should I do?") + return Command(goto="next", update={"answer": answer}) +``` + +```rust +// Rust +use graph_flow::compat::Command; +use graph_flow::{NextAction, TaskResult}; + +// GoTo +Ok(TaskResult::new(Some("done".to_string()), NextAction::GoTo("next".to_string()))) + +// WaitForInput (interrupt equivalent) +Ok(TaskResult::new(Some("What should I do?".to_string()), NextAction::WaitForInput)) + +// Command enum (for programmatic use) +let cmd = Command::goto("next"); +let cmd = Command::update(serde_json::json!({"answer": "yes"})); +let cmd = Command::resume(serde_json::json!("user input")); +``` + +### Channels / Reducers + +```python +# Python +from typing import Annotated +from langgraph.graph import add_messages + +class State(TypedDict): + messages: Annotated[list[BaseMessage], add_messages] # Append reducer + count: Annotated[int, lambda a, b: a + b] # Custom reducer + name: str # LastValue (default) +``` + +```rust +// Rust +use graph_flow::{Channels, ChannelReducer, ChannelConfig}; + +let mut channels = Channels::new(); +channels.register("messages", ChannelReducer::Append); +channels.register("count", ChannelReducer::Custom(Arc::new(|a, b| { + let a = a.as_i64().unwrap_or(0); + let b = b.as_i64().unwrap_or(0); + serde_json::json!(a + b) +}))); +channels.register("name", ChannelReducer::LastValue); +``` + +### Subgraphs + +```python +# Python +inner = StateGraph(InnerState) +inner.add_node("process", process_fn) +inner.add_edge(START, "process") +inner_compiled = inner.compile() + +outer = StateGraph(OuterState) +outer.add_node("sub", inner_compiled) # Subgraph as node +outer.add_edge(START, "sub") +``` + +```rust +// Rust +use graph_flow::subgraph::SubgraphTask; + +let inner_graph = Arc::new( + GraphBuilder::new("inner") + .add_task(Arc::new(ProcessTask)) + .build() +); + +let subgraph = SubgraphTask::new("sub", inner_graph); + +let outer_graph = GraphBuilder::new("outer") + .add_task(subgraph) + .set_start_task("sub") + .build(); +``` + +### ReAct Agent + +```python +# Python +from langgraph.prebuilt import create_react_agent + +agent = create_react_agent( + model=ChatOpenAI(model="gpt-4"), + tools=[search_tool, calculator_tool], + prompt="You are a helpful assistant" +) +result = agent.invoke({"messages": [("user", "What is 2+2?")]}) +``` + +```rust +// Rust +use graph_flow::create_react_agent; + +let agent_graph = create_react_agent( + Arc::new(LlmTask::new("gpt-4")), + vec![ + Arc::new(SearchTool) as Arc, + Arc::new(CalculatorTool) as Arc, + ], + 10, // max iterations +); + +let mut session = Session::new_from_task("thread".to_string(), "llm"); +session.context.add_user_message("What is 2+2?".to_string()).await; +let result = agent_graph.execute_session(&mut session).await?; +``` + +### RetryPolicy + +```python +# Python +from langgraph.types import RetryPolicy + +policy = RetryPolicy( + initial_interval=1.0, + max_interval=10.0, + backoff_multiplier=2.0, + max_attempts=3 +) +``` + +```rust +// Rust +use graph_flow::{RetryPolicy, BackoffStrategy}; +use std::time::Duration; + +let policy = RetryPolicy::exponential( + 3, // max_retries + Duration::from_secs(1), // base delay + Duration::from_secs(10), // max delay +); + +// Or fixed backoff +let policy = RetryPolicy::fixed(3, Duration::from_secs(2)); +``` + +### HTTP API + +```python +# Python (LangGraph Platform client) +from langgraph_sdk import get_client + +client = get_client(url="http://localhost:8123") +thread = client.threads.create() +run = client.runs.create(thread["thread_id"], assistant_id="agent", input={"messages": [...]}) +state = client.threads.get_state(thread["thread_id"]) +``` + +```rust +// Rust (server side) +use graph_flow_server::create_router; + +let app = create_router(graph, storage); +let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?; +axum::serve(listener, app).await?; + +// Client side (HTTP requests) +// POST /threads → CreateThreadRequest { start_task, context } +// POST /threads/{id}/runs → (no body) → RunResponse +// GET /threads/{id}/state → StateResponse +// DELETE /threads/{id} → 204 No Content +``` + +--- + +## Error Mapping + +| Python Exception | Rust Error | Pattern | +|-----------------|-----------|---------| +| `GraphRecursionError` | `GraphError::TaskExecutionFailed("exceeded recursion limit")` | Via RunConfig | +| `InvalidUpdateError` | `GraphError::TaskExecutionFailed(msg)` | | +| `GraphInterrupt` | `NextAction::WaitForInput` + `ExecutionStatus::WaitingForInput` | | +| `NodeInterrupt` | MISSING — use `NextAction::WaitForInput` with context data | | +| `TaskNotFound` | `GraphError::TaskNotFound(id)` | | +| `EmptyChannelError` | `GraphError::ContextError(msg)` | | +| Generic errors | `GraphError::Other(anyhow::Error)` | Via `#[from]` | + +--- + +## Idiom Mapping + +| Python Pattern | Rust Pattern | +|---------------|-------------| +| `state["key"]` | `ctx.get::("key").await` | +| `return {"key": value}` | `ctx.set("key", value).await` | +| `config["configurable"]["thread_id"]` | `session.id` | +| `@tool` decorator | `impl Task for MyTool` | +| `yield` in generator | `sender.send(StreamChunk{...}).await` | +| `async for chunk in stream` | `while let Some(chunk) = stream.next().await` | +| `TypedDict` with `Annotated[T, reducer]` | `Channels::register(key, ChannelReducer)` | +| `Runnable.with_config()` | `FlowRunner::run_with_config(id, &config)` | +| `MemorySaver()` | `InMemorySessionStorage::new()` | +| `interrupt("question")` | `NextAction::WaitForInput` + response in context |