Chapter 5 of 5 · Operational Excellence
Generated with AI

Configuration, versioning & memory

Once an agent runs in production, the operational questions narrow to a few sharp ones: where configuration and secrets come from at startup, how easily you can swap the underlying LLM / memory store / vector backend / session store without rewriting your agent code, what memory primitives ship for short-term and long-term recall, and how sessions get isolated across users and tenants. This chapter walks through each axis through a production-runtime lens, with a particular focus on how pluggable each framework's default abstractions actually are when you need to change the implementation underneath them.

What's in this chapter

1Agent definition & prompt versioning

How is an agent defined — Python code, a YAML file, an interface declaration, a prompt file? And once defined, how do you version, A/B test, or roll back a prompt without redeploying the whole service? Most frameworks treat agents as code (versioned through your VCS); a few use declarative formats; and only two ship a version-aware artifact registry for prompts.

What this looks like in practice

It's Friday afternoon. Your CRO wants you to A/B test two variants of the customer- service agent's opening prompt. With a code-defined agent, you ship two builds and a feature flag — every prompt change is a code change, a code review, a deploy. With a prompt-registry like LangChain Hub, you push a new prompt version, route 10% of traffic to it via the dashboard, and roll back from the same dashboard if metrics regress. With Genkit's .prompt files, you swap a file in your repo, the dev server hot-reloads, and a deploy ships the change — no separate registry but clean file-level versioning. The other frameworks land between these poles.

FrameworkDefinition stylePrompt versioning surfaceSource
LangChainPython Code — Runnable chain composition; no dedicated agent class. LangChain Hub — versioned prompt registry with named, taggable prompts. langchain.hub
LangGraphPython Code — StateGraph + node functions; state schema as TypedDict / Pydantic. Inherits LangChain Hub. —
CrewAIPython Code — Pydantic Agent class with role, goal, backstory fields. None — code-versioned only. —
Google ADKPython YAML or code — LlmAgentConfig Pydantic schema declares agents in YAML; LlmAgent instantiates from config or constructor. YAML file in your repo; no separate registry. src/google/adk/agents/llm_agent_config.py:35
Semantic KernelPython Code — Kernel + @kernel_function decorators on plugin classes. None — code-versioned only. —
MastraTypeScript Code — new Agent({ id, name, instructions, model, ... }). None — code-versioned only. —
MS Agent Framework.NET YAML or code — declarative workflow files (kind: InvokeAzureAgent); also code-based agents via DI. YAML file in your repo; Azure Foundry hosts named agents externally. declarative-agents/ (samples)
LangChain4jJava Interface — @AiService annotated Java interface; methods map to LLM calls. Built via AiServices.builder(...). None — code-versioned only. langchain4j/.../service/AiServices.java
GenkitGo Mixed — prompts in .prompt files (Dotprompt format) + flows as Go functions. .prompt files — file-based, hot-reload aware, versioned alongside code. go/ai/prompt.go
RigRust Code — Agent::builder().model(...).system_prompt(...).build(). None — code-versioned only. —

What this means in practice

Three patterns dominate. Code-first (LangChain, LangGraph, CrewAI, SK, Mastra, Rig) is the most common — agents are Python / TypeScript / Rust objects, versioning is whatever VCS you use. Declarative YAML (ADK, MS Agent Framework) treats agent definitions as data files — same VCS-versioned story but with a clearer separation between "what the agent is" and "what surrounds it." Interface-based (LangChain4j) is unique to the JVM — agents are Java interfaces with annotations, and the framework synthesises the implementation at runtime.

On prompt versioning as a separate artifact: LangChain Hub is the most feature-rich — named prompts with version tags and dashboard-driven A/B routing. Genkit Go's .prompt files are file-based but hot-reload aware, which is a nicer dev experience than a registry but lacks the runtime A/B story. Everyone else treats prompts as code: change the prompt, ship a new build.

2Configuration & secrets at startup

In production, configuration arrives from environment variables, config files, secret managers, or your platform's parameter store — and you switch between dev, staging, and production without changing code. The frameworks fall into three groups: ones that inherit a complete config story from their host (LangChain4j on Spring/Quarkus, MS Agent Framework on .NET), ones that use their own structured pattern (ADK's YAML), and ones that mostly leave configuration loading to you.

What this looks like in practice

Your production deployment reads a database password from AWS Secrets Manager, the OpenAI key from a Kubernetes Secret, the model name and timeout from a ConfigMap, and the tenant routing rules from a feature-flag service. With Spring Boot and LangChain4j or ASP.NET Core and MS Agent Framework, the host framework's config binder picks all of these up through its standard providers — secrets, configmaps, envs, and profiles compose into a single typed configuration. With most Python frameworks, your bootstrap code has to wire each source explicitly, often through Pydantic Settings or Hydra. The differences mostly reflect which host ecosystem each framework targets.

The other production concern is secrets: API keys for providers, credentials for vector stores, tokens for downstream tools. None of the ten frameworks ships a secrets-manager integration — they all read from environment variables and expect your platform layer (Vault, Kubernetes Secrets, AWS Secrets Manager, Azure Key Vault) to populate those vars before the process starts.

FrameworkConfig sourcesMulti-environment storySource
LangChainPython Env vars + runtime config dicts passed to invoke(). None framework-managed — bring Pydantic Settings, Hydra, or env-based bootstrap. —
LangGraphPython Same as LangChain plus a config dict at invoke()/astream() carrying thread_id, checkpoint_id, etc. Same as LangChain. —
CrewAIPython Pydantic Agent / Crew constructors; env vars for provider keys. None framework-managed. —
Google ADKPython YAML config (LlmAgentConfig) drives agent definition; env vars for Google Cloud auth. YAML files per environment; load the right one at startup. llm_agent_config.py
Semantic KernelPython Services registered via kernel.add_service() at init; env discovery via OpenAIChatCompletion.from_env() factory pattern. None framework-managed. —
MastraTypeScript Constructor arguments + process.env in setup code; deployer-specific env handling for Vercel / Cloudflare / etc. Each deployer adapter handles its target's env model. packages/deployer/
MS Agent Framework.NET Standard .NET config — appsettings.json, environment-specific overrides, DI container; agent registration in Program.cs via hosting extensions. Built-in via .NET — appsettings.{Environment}.json override pattern. Microsoft.Agents.AI.Hosting.*
LangChain4jJava Spring Boot application.yml / Quarkus application.properties via the host framework's starter. Built-in via Spring Boot profiles or Quarkus profiles. Spring Boot starter / quarkus-langchain4j
GenkitGo Go config to genkit.New(); env vars per provider plugin; prompts loaded from disk. Standard Go env-var pattern (os.Getenv); Go build tags or env switches. —
RigRust Builder pattern + std::env::var(); whatever crates you bring (e.g. config, figment). None framework-managed. —

What this means in practice

Two frameworks inherit a complete config story from their host: LangChain4j (Spring Boot or Quarkus profiles) and MS Agent Framework (.NET appsettings + environments). ADK comes close with its YAML LlmAgentConfig. The other seven leave configuration to you — read env vars, pick the right values for the right environment, and wire it through. Python frameworks especially benefit from adding Pydantic Settings or Hydra; Mastra benefits from its deployer's per-target env handling.

3Implementation swap-ability

Production realities make you swap implementations all the time: the model provider changes, the vector store changes, the memory backend has to move on-prem, the session store needs to shard, the tracing exporter has to switch vendors. The question isn't whether the framework has a default for these — they all do — but how easy it is to provide your own implementation when the default doesn't fit. The answer comes down to how broadly the framework exposes its internals as pluggable abstractions, and how cleanly you can register a custom one.

What this looks like in practice

Your security team mandates that all vectors live in your internal vector database, not Pinecone. With a framework whose vector layer is an abstract base class with five-or-so methods (add, search, delete, etc.), you implement the interface against your internal DB, register your class, and the rest of the agent code never knows the swap happened — maybe an afternoon of work. With a framework where the vector layer is hard-coded inside agent internals, you're forking the framework, patching multiple files, and maintaining the diff forever.

Three things matter for swap-ability: (1) which abstractions are pluggable — LLM provider, memory store, vector store, session/checkpoint store, tool registry, tracing exporter, cache backend; (2) how the interface is shaped — a small clean interface is easier than a sprawling abstract class with assumptions baked in; (3) how you register a custom implementation — a global setter, DI registration, builder argument, or trait impl.

FrameworkPluggable abstractionsEffort to swap a defaultSource
LangChainPython Nearly every component — BaseChatModel, BaseLLM, BaseTool, BaseCache, BaseChatMessageHistory, VectorStore, BaseRetriever, BaseLoader, BaseCallbackHandler. Low — clear abstract bases with dozens of reference implementations to crib from; a custom impl is typically 30–80 lines. core/langchain_core/
LangGraphPython BaseCheckpointSaver for durable state (memory / SQLite / Postgres / your own); inherits LangChain's full surface for everything else. Low — checkpoint contract is small (put / get / list); reuses LangChain abstractions for models, tools, etc. libs/checkpoint/langgraph/checkpoint/base/
CrewAIPython Memory Storage interface; tool subclassing; LLM provider goes through LiteLLM (which is itself a unification layer). Moderate — fewer abstractions exposed; the LiteLLM dependency limits how cleanly you can replace the model layer with a non-LiteLLM client. src/crewai/memory/storage/
Google ADKPython BaseSessionService, BaseMemoryService, BaseArtifactService, BaseAgent, BaseTool, BaseLlm, plus a plugin system for runtime hooks. Low — abstractions are well-defined; in-memory + Database + SQLite + Vertex backends already ship as reference implementations. src/google/adk/sessions/base_session_service.py · memory/
Semantic KernelPython Service interfaces (ChatCompletionClientBase, EmbeddingGeneratorBase, etc.); connectors as separate packages per vendor. Moderate — requires understanding the kernel's service registration model; the connector pattern is consistent once you've seen one. python/semantic_kernel/services/
MastraTypeScript MastraServerCache, MastraMemory, abstract provider bases; vector / cache / storage backends ship as separate stores/ packages. Low — abstract bases follow a consistent pattern; provider/store packages show how to extend them. packages/core/src/ · stores/
MS Agent Framework.NET AIAgent base class, IChatClient (from Microsoft.Extensions.AI), agent-thread interfaces — all registered via the .NET DI container. Low — standard .NET DI patterns; everything is interface-based and DI registration is one line in Program.cs. dotnet/src/Microsoft.Agents.AI.Abstractions/AIAgent.cs
LangChain4jJava ChatModel, EmbeddingModel, EmbeddingStore, ChatMemoryStore, ContentRetriever — all interfaces. ~80 modules show how each is implemented per vendor. Low — interfaces are small and uniform across the catalogue; pick the closest existing module and adapt. langchain4j-core/src/main/java/dev/langchain4j/
GenkitGo Plugin interface; genkit.DefineModel, genkit.DefineEmbedder, genkit.DefineEvaluator register custom implementations. Moderate — plugin pattern requires understanding the registry lifecycle; once registered, custom implementations are first-class. go/core/ · go/plugins/
RigRust CompletionModel, EmbeddingModel, VectorStoreIndex traits. Low — idiomatic Rust trait pattern; implement the trait, the rest of the agent stack works unchanged. crates/rig-core/src/

What this means in practice

Most frameworks land at "Low effort to swap a default." LangChain has the broadest abstraction surface, with abstract base classes for nearly every component and a large library of reference implementations to copy. LangChain4j is the JVM equivalent — clean interfaces and ~80 vendor modules to learn from. ADK has the most explicitly named service interfaces (Session, Memory, Artifact). Rig's trait-based design is idiomatic Rust and stays out of the way. MS Agent Framework leans on standard .NET DI, which makes registration trivial.

Two frameworks have moderate friction. CrewAI exposes fewer abstractions and routes the model layer through LiteLLM, which means swapping providers is easy (LiteLLM supports many) but swapping away from LiteLLM requires more plumbing. Semantic Kernel's service-registration model is consistent but requires more setup than a simple subclass-and-register. Genkit Go's plugin registry is powerful but expects you to learn its lifecycle first. None of these are show-stoppers; they're just more upfront learning.

4Memory architectures

Conversations need memory: what the user said earlier in the session (short-term), what the user told the agent last week (long-term episodic), and what's relevant from the company knowledge base (semantic / vector). Six of the ten frameworks ship a first-class memory abstraction; the others delegate to whatever you wire below them.

What this looks like in practice

Two memory primitives matter most. Short-term: the running list of messages in this conversation, often capped by token count. Long-term: facts the agent should remember across sessions — user preferences, prior decisions, contextual details — typically backed by a vector store so the agent can semantically recall relevant items. A framework that ships both lets you write agent({ memory: Memory({...}) }); a framework without memory primitives leaves you to thread message history in/out per request and integrate a vector store yourself.

FrameworkShort-term memoryLong-term / vector-backed memorySource
LangChainPython BaseChatMessageHistory implementations (in-memory, SQL, Redis, MongoDB). Vector-store integrations (Chroma, Pinecone, pgvector, Qdrant, Weaviate, ~50 stores). langchain_core.chat_history · langchain.vectorstores
LangGraphPython MessagesState with add_messages reducer; checkpointer persists full graph state per thread. Inherits LangChain vector stores; checkpointer can store arbitrary state including embeddings. libs/langgraph/langgraph/graph/message.py
CrewAIPython Memory class with MemoryScope (agent / crew / task / user); short-term conversation buffer. Vector-backed semantic recall via LanceDB; lazy EncodingFlow writes embeddings. src/crewai/memory/
Google ADKPython BaseSessionService with concrete InMemorySessionService, DatabaseSessionService, SqliteSessionService, VertexAiSessionService. BaseMemoryService for vector-backed long-term memory. src/google/adk/sessions/ · memory/
Semantic KernelPython ChatHistory per-kernel — caller manages lifecycle. None core — vector-store connectors are separate (Azure AI Search, Qdrant, etc.). python/semantic_kernel/contents/chat_history.py
MastraTypeScript Memory class extending MastraMemory; thread-scoped working memory and message history. Built-in semantic recall via embeddings; pluggable vector stores. packages/memory/src/index.ts:212
MS Agent Framework.NET Agent threads track conversation; backend-delegated when running on Azure Agent Service. Backend-delegated; Foundry / Azure Agent Service hosts long-term memory. Microsoft.Agents.AI/
LangChain4jJava ChatMemory interface — MessageWindowChatMemory (count-bounded), TokenWindowChatMemory (token-bounded). Storage via ChatMemoryStore. Embedding stores (~30 vendors) integrated separately from chat memory. langchain4j/.../memory/chat/
GenkitGo Not built into core — flows are stateless; conversation state passed as function parameter. Plugins for vector stores (Pinecone, Chroma, etc.) but not bundled into a "memory" abstraction. —
RigRust Caller manages message history; agent stateless by default. Separate rig-memory crate with vector-store integration (LanceDB, in-memory). crates/rig-memory/

What this means in practice

Six frameworks ship first-class memory primitives: LangChain (broadest vector-store catalogue), LangGraph (state-as-memory via checkpointer), CrewAI (scoped memory with LanceDB-backed semantic recall), ADK (session service + memory service abstractions with multiple backends), Mastra (thread-scoped Memory with built-in semantic recall), and LangChain4j (window-based chat memory + ~30 embedding stores). Semantic Kernel and Rig provide chat-history primitives but no memory abstraction — you wire vector stores yourself. MS Agent Framework's memory story is backend-delegated when running on Azure Agent Service; Genkit Go is stateless by design.

5Multi-tenancy & session isolation

Production agents serve many users — and many tenants. How do you keep one user's conversation history out of another user's run? The frameworks have converged remarkably on a single pattern: a "thread id" string passed at request time that isolates session state. The naming differs (thread_id, threadId, conversationId, session_id); the shape is the same.

What this looks like in practice

A multi-tenant SaaS agent serves 50 customer organisations. Each end-user within a customer has their own conversation history. With a thread-id pattern, your gateway passes thread_id="acme-org/jane-doe-session-42" in the request config; the agent runtime looks up state for that thread, runs the agent, persists state back. Different threads never see each other's data, and you can shard storage by thread prefix if scale demands. With no session primitive, you thread message history in/out manually and risk getting it wrong on a future code change.

FrameworkIsolation primitiveHow it's usedSource
LangChainPython RunnableWithMessageHistory + get_session_history(session_id) callback. Caller passes session_id at invoke(); framework calls back for history. langchain_core.runnables.history
LangGraphPython thread_id in the runnable config dict. Checkpointer keys all state by thread; graph.invoke(input, {"configurable": {"thread_id": "..."}}). libs/langgraph/langgraph/_internal/_config.py
CrewAIPython Per-crew instance; memory scoped per agent / crew / task / user. Run a fresh Crew per session, or use the user memory scope to isolate within a long-lived crew. memory/memory_scope.py
Google ADKPython Session with user_id + session_id. Session service stores per-user state; thread-per-session pattern. src/google/adk/sessions/base_session_service.py
Semantic KernelPython Per-kernel ChatHistory. Caller maintains a kernel + chat history per user; no built-in routing. —
MastraTypeScript threadId + resourceId on memory operations. Agent calls scope all reads/writes to the thread; thread-history is cloneable and resumable. packages/memory/
MS Agent Framework.NET conversationId at workflow invocation. Agents stateless; conversation ID tracks the thread; backend persists per-conversation state. —
LangChain4jJava chatMemoryProvider(Function<String, ChatMemory>) callback. Memory ID (user / session) passed at request time; provider returns per-ID ChatMemory. AiServices.builder(...).chatMemoryProvider(...)
GenkitGo Not modelled — flows stateless; caller passes context as function parameter. Roll your own session ID + state lookup at the flow boundary. —
RigRust Not modelled — agents are single-user by default. Multi-tenancy requires app-level threading and context isolation. —

What this means in practice

Eight frameworks expose a thread-id-shaped primitive — the names vary but the shape is the same: a string passed at request time that namespaces state and isolates one session from another. LangGraph (thread_id), Mastra (threadId), MS Agent Framework (conversationId), ADK (Session with user_id), CrewAI (memory scope or per-crew instance), LangChain (session_id callback), LangChain4j (chatMemoryProvider callback), and SK (per-kernel chat history). Genkit Go and Rig leave session isolation to your application code — they're stateless by design.

At a glance

Source-grounded observations on configuration, versioning, and memory surfaces across the ten frameworks.

DEFINITION

1 · Code-first dominates; YAML and interface are the variants

Six frameworks define agents in code (LangChain, LangGraph, CrewAI, SK, Mastra, Rig). ADK and MS Agent Framework support YAML declarative formats. LangChain4j is unique with its @AiService Java interface pattern. Genkit Go is mixed — code flows + .prompt files.

PROMPT VERSIONING

2 · Only two ship version-aware prompt artifacts

LangChain Hub is the most feature-rich — named prompts with version tags and dashboard-driven A/B routing. Genkit Go's .prompt files are file-based and hot-reload aware. Everyone else versions prompts as code through your VCS.

CONFIG

3 · Config story tracks the host ecosystem

LangChain4j and MS Agent Framework inherit complete config solutions from Spring Boot / Quarkus / .NET. ADK's YAML LlmAgentConfig is the closest first-class story among Python frameworks. The other seven leave config to you — add Pydantic Settings, Hydra, or per-deployer adapters.

SWAP-ABILITY

4 · Most frameworks make swap-ability cheap

Seven frameworks land at "Low effort to swap a default" — LangChain (broadest abstraction surface), LangGraph (BaseCheckpointSaver + LangChain's surface), ADK (named Session/Memory/Artifact services), Mastra (consistent abstract bases + store packages), MS Agent Framework (.NET DI), LangChain4j (~80 interface-based modules), and Rig (trait-based). CrewAI, SK, and Genkit Go land at moderate friction because of their respective LiteLLM/connector/plugin patterns.

MEMORY

5 · First-class memory ships in six

LangChain (50+ vector stores), LangGraph (state-as-memory via checkpointer), CrewAI (scoped memory + LanceDB), ADK (Session + Memory services with multiple backends), Mastra (thread-scoped Memory with semantic recall), LangChain4j (window-based chat memory + ~30 embedding stores). SK, MS Agent Framework, Genkit Go, and Rig delegate.

MULTI-TENANCY

6 · Thread-id is the convergent pattern

Eight frameworks expose a thread-id-shaped primitive — different names (thread_id, threadId, conversationId, session_id, user_id) but identical shape: a string passed at request time that namespaces state. Genkit Go and Rig leave session isolation to your application code.