Skip to main content

Conversation Primitives

Grid provides a hierarchical set of conversation primitives that enable flexible conversation management. Each primitive can be used independently - you don't need to use all layers. All primitives use a closure-based functional pattern - no classes, just functions returning objects with methods.

Independence by Design

Important: While these primitives work beautifully together, each one is designed to function independently:

  • Use only what you need - Don't want the manager? Use atomic primitives directly
  • No forced dependencies - Each layer is optional
  • Mix and match - Combine primitives however suits your needs
  • Future-proof - Start simple, add layers as requirements grow

Architecture Overview

Grid's conversation primitives are organized in three layers:

┌─────────────────────────────────────────┐
│ Organism Level │
│ (createConversationLoop) │ ← Full Orchestration
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
│ Composed Level │
│ (createConversationManager) │ ← Unified Interface
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
│ Atomic Level │
│ (createConversationHistory) │ ← Message Storage
│ (createConversationContext) │ ← State Management
└─────────────────────────────────────────┘

Closure-Based Pattern

All primitives follow the same implementation pattern:

export const createPrimitive = (options?: PrimitiveOptions) => {
// Private state in closure
const privateState = { /* ... */ };

// Private helper functions
const helperFunction = () => { /* ... */ };

// Return public API
return {
publicMethod: () => {
// Access and modify private state
return /* ... */;
},
anotherMethod: () => { /* ... */ },
};
};

Atomic Primitives

createConversationHistory

Manages the storage and retrieval of conversation messages with optional event handlers.

import { createConversationHistory } from "@mrck-labs/grid-core";

const history = createConversationHistory("You are a helpful assistant", {
// Optional event handlers for persistence
onMessageAdded: async (message) => {
console.log("Message added:", message);
await database.messages.create({ data: message });
},
onMessagesCleared: async () => {
console.log("Messages cleared");
await database.messages.deleteMany({ sessionId });
},
});

// Add messages
history.addMessage({ role: "user", content: "Hello!" });
history.addMessage({
role: "assistant",
content: "Hi there! How can I help?"
});

// Add tool responses
history.addToolResponse(
"call_123", // toolCallId
"get_weather", // toolName
"72°F and sunny" // result
);

// Access messages
const all = history.getMessages();
const nonSystem = history.getNonSystemMessages();
const lastUser = history.getLastMessageByRole("user");

// Message metrics
const userCount = history.getMessageCountByRole("user");
const hasMessages = history.hasMessages();

// Clear (preserves system message, triggers onMessagesCleared)
history.clear();

Key Features:

  • System message preservation
  • Tool response handling
  • Message filtering by role
  • Immutable message access (returns copies)

createConversationContext

Manages contextual information and conversation metadata with optional event handlers.

import { createConversationContext } from "@mrck-labs/grid-core";

const context = createConversationContext({
// Optional event handlers for persistence
onStateChanged: async (key, value) => {
console.log(`State changed: ${key} = ${value}`);
await database.state.upsert({ key, value });
},
onMetadataChanged: async (key, value) => {
console.log(`Metadata changed: ${key} = ${value}`);
await database.metadata.upsert({ key, value });
},
});

// State management (triggers onStateChanged)
context.updateState("user.name", "Alice");
context.updateStates({
"user.preferences.language": "en",
"user.preferences.timezone": "PST",
"session.id": "sess_123",
});

// Access state
const state = context.getState(); // Returns copy
const userName = context.getStateValue("user.name");

// Metadata tracking (triggers onMetadataChanged)
context.updateMetadata("topic", "weather");
context.incrementMessageCount();
context.incrementToolCallCount(2);

// Get metrics
const metrics = context.getMetrics();
// { messageCount: 5, toolCallCount: 2, startTime: Date, ... }

// Snapshot for persistence
const snapshot = context.getSnapshot();

Key Features:

  • Nested state management
  • Automatic metric tracking
  • Metadata for categorization
  • Snapshot/restore capability

Composed Primitives

createConversationManager

Combines history and context into a unified interface with grouped event handlers.

import { createConversationManager } from "@mrck-labs/grid-core";

const manager = createConversationManager({
historyOptions: {
systemPrompt: "You are a helpful assistant"
},
handlers: {
// Grouped handlers for clean organization
manager: {
onUserMessageAdded: async (message) => {
await analytics.track("user_message", { message });
},
onAgentResponseProcessed: async (response) => {
await analytics.track("agent_response", response);
},
onToolResponseAdded: async (toolCallId, toolName, result) => {
await analytics.track("tool_executed", { toolName, result });
},
},
// Delegate to underlying primitive handlers
history: {
onMessageAdded: async (message) => {
await database.messages.create({ data: message });
},
},
context: {
onStateChanged: async (key, value) => {
await database.state.upsert({ key, value });
},
},
});

// Unified message handling (triggers handlers)
manager.addUserMessage("What's the weather?");

// Process agent responses
manager.processAgentResponse({
content: "I'll check the weather for you.",
toolCalls: [{
id: "call_123",
name: "get_weather",
args: { location: "SF" },
}],
});

// Add tool results
manager.addToolResponse("call_123", "get_weather", "72°F");

// Combined state access
const state = manager.getConversationState();
// {
// messages: [...],
// context: { ... },
// metrics: { messageCount: 3, ... }
// }

// Get summary
const summary = manager.getSummary();
// {
// messageCount: 3,
// lastUserMessage: "What's the weather?",
// lastAssistantMessage: "I'll check...",
// hasToolCalls: true
// }

// State management (delegated to context)
manager.updateState("weather.location", "San Francisco");

// Reset everything
manager.reset();

Key Features:

  • Automatic metric tracking
  • Unified interface for history and context
  • Convenient summary generation
  • Method delegation to underlying services

Organism Primitives

createConversationLoop

Orchestrates complete conversation flows with agent integration and lifecycle event handlers.

import { createConversationLoop } from "@mrck-labs/grid-core";

const loop = createConversationLoop({
agent: myAgent,
handlers: {
// Lifecycle event handlers
onConversationStarted: async ({ sessionId, userId, conversationId }) => {
console.log("Conversation started", { sessionId });
// Load previous messages from database
const messages = await database.messages.findMany({
where: { sessionId }
});
return { initialMessages: messages };
},
onMessageSent: async (message, context) => {
console.log("Message sent:", message);
await database.audit.create({
type: "message_sent",
content: message,
context,
});
},
onResponseReceived: async (response, context) => {
console.log("Response received:", response);
await database.audit.create({
type: "response_received",
response,
context,
});
},
onConversationEnded: async (summary, context) => {
console.log("Conversation ended", summary);
await database.sessions.update({
where: { id: context.sessionId },
data: { summary, endedAt: new Date() },
});
},
},
});

// Send messages with automatic tool resolution
const response = await loop.sendMessage("What's the weather in Paris?");
console.log(response.content);

// Analytics
const analytics = loop.getAnalytics();
// {
// messageCount: 4,
// toolCallCount: 3,
// turnCount: 2,
// duration: 45000,
// isActive: true
// }

// Export/Import conversations
const exported = loop.exportConversation();
// Save to database...

// Later...
const newLoop = createConversationLoop(options);
newLoop.importConversation(exported);

// Lifecycle management (triggers handlers)
loop.endConversation();
loop.resetConversation();

Key Features:

  • Automatic tool call resolution
  • Turn counting and analytics
  • Export/import for persistence
  • Conversation lifecycle management
  • Multi-round tool execution

Independent Usage Examples

Using Only ConversationHistory

Perfect when you just need message storage:

// No manager, no loop - just history
const history = createConversationHistory("System prompt");

// Use it directly for logging, auditing, or simple chats
await history.addMessage({ role: "user", content: "Hello" });
await history.addMessage({ role: "assistant", content: "Hi!" });

// That's it! No other primitives required
const messages = history.getMessages();

Using Only ConversationContext

Great for state management without message tracking:

// Just context - no history needed
const context = createConversationContext();

// Perfect for tracking user preferences, form state, etc.
await context.updateState("form.step", 1);
await context.updateState("user.preferences", { theme: "dark" });

// Access your state
const currentStep = context.getStateValue("form.step");

Building Your Own Manager

Skip ConversationManager and build exactly what you need:

// Custom combination of primitives
function createMyCustomFlow() {
const history = createConversationHistory();
const context = createConversationContext();

return {
// Only expose what you need
addMessage: (msg: string) => history.addMessage({ role: "user", content: msg }),
getState: () => context.getState(),
updatePreference: (key: string, value: any) =>
context.updateState(`prefs.${key}`, value)
};
}

Usage Patterns

Simple Conversation

// For basic Q&A
const history = createConversationHistory();
history.addMessage({ role: "user", content: "Hello" });
history.addMessage({ role: "assistant", content: "Hi!" });

Stateful Conversation

// For conversations needing context
const manager = createConversationManager();
manager.addUserMessage("My name is Alice");
manager.updateState("user.name", "Alice");
manager.processAgentResponse({
content: "Nice to meet you, Alice!",
});

Agent-Powered Conversation

// For full agent integration
const llmService = baseLLMService({ langfuse: { enabled: false } });
const toolExecutor = createToolExecutor();

const agent = createConfigurableAgent({
llmService,
toolExecutor,
config: {
id: "travel-planner",
type: "general",
version: "1.0.0",
prompts: {
system: "You are a helpful travel planning assistant."
},
metadata: {
id: "travel-planner",
type: "general",
name: "Travel Planner",
description: "Helps plan trips and travel itineraries",
capabilities: ["general"],
version: "1.0.0"
},
tools: {
builtin: [],
custom: [],
mcp: []
},
behavior: {
maxRetries: 3,
responseFormat: "text"
}
}
});

const loop = createConversationLoop({ agent });
const response = await loop.sendMessage("Help me plan a trip");

Production Conversation

// For production with monitoring
const loop = createConversationLoop({
agent: productionAgent,
onProgress: (update) => {
logger.info("Progress", update);
websocket.emit("progress", update);
},
});

Best Practices

1. Choose the Right Level

  • Use atomic primitives for fine-grained control
  • Use composed primitives for standard use cases
  • Use organism primitives for complete solutions

2. Leverage Closure Benefits

// Private state is truly private
const history = createConversationHistory();
// No way to directly access internal messages array
// Must use public methods

3. Composition Over Inheritance

// Compose primitives for custom behavior
const createCustomFlow = (options) => {
const loop = createConversationLoop(options);
const analytics = createAnalyticsTracker();

return {
...loop,
sendMessage: async (msg) => {
analytics.track("message.sent");
const response = await loop.sendMessage(msg);
analytics.track("message.completed");
return response;
},
};
};

4. Handle Errors Gracefully

const loop = createConversationLoop({
onProgress: (update) => {
if (update.type === "error") {
errorReporter.log(update.message);
}
},
});

5. Use TypeScript

All primitives are fully typed:

import type { 
ConversationHistory,
ConversationContext,
ConversationManager,
ConversationLoop,
} from "@mrck-labs/grid-core";

Migration Guide

If coming from class-based systems:

// Instead of:
const history = new ConversationHistory();

// Use:
const history = createConversationHistory();

// Instead of:
class CustomHistory extends ConversationHistory { }

// Use composition:
const createCustomHistory = () => {
const base = createConversationHistory();
return {
...base,
customMethod: () => { /* ... */ },
};
};

Next Steps