Multi-Turn Conversations

Updated

The Conversation class manages multi-turn interactions with the Chaos AI API. It automatically tracks message history, handles session IDs, and supports branching and persistence -- so you can focus on the dialogue itself rather than state management.

Creating a Conversation

A Conversation wraps a Chaos client and a set of options. At minimum you need a userId.

basic-conversation.ts
import { Chaos, Conversation } from '@chaoslabs/ai-sdk';
 
const chaos = new Chaos({ apiKey: process.env.CHAOS_API_KEY! });
 
const conversation = new Conversation(chaos, {
  userId: 'user-123',
  wallets: [
    { address: '0xYourWalletAddress', chain: 'ethereum' },
  ],
});

ConversationOptions

OptionTypeRequiredDescription
userIdstringYesUnique user identifier
walletsArray<{ address: string; chain: string }>NoWallet addresses across chains
modelstringNoModel override (defaults to WALLET_MODEL)
maxHistoryLengthnumberNoMax messages kept in history (default 50)
sessionIdstringNoExplicit session ID (auto-generated if omitted)
walletContextOverrideWalletContextOverrideNoOverride wallet context for deterministic testing

Sending Messages

Call send(message) to send a user message and receive a ChatCreateResponse. The conversation automatically appends both the user message and the assistant reply to its internal history, so subsequent calls carry the full context.

send-messages.ts
import { Chaos, Conversation, extractText } from '@chaoslabs/ai-sdk';
 
const chaos = new Chaos({ apiKey: process.env.CHAOS_API_KEY! });
 
const conversation = new Conversation(chaos, {
  userId: 'user-123',
  wallets: [{ address: '0xABC...', chain: 'ethereum' }],
});
 
// First turn
const response1 = await conversation.send("What's my portfolio value?");
console.log(extractText(response1));
 
// Second turn -- the API sees full history automatically
const response2 = await conversation.send('Which asset has the highest allocation?');
console.log(extractText(response2));
 
// Third turn -- references both prior answers
const response3 = await conversation.send('Should I rebalance?');
console.log(extractText(response3));
[@portabletext/react] Unknown block type "callout", specify a component for it in the `components.types` prop

Conversation Stats

The stats getter returns a snapshot of the conversation state.

conversation-stats.ts
const stats = conversation.stats;
 
console.log(stats.sessionId);      // "session-1719...-a3f2k1"
console.log(stats.userTurns);       // 3
console.log(stats.assistantTurns);  // 3
console.log(stats.totalMessages);   // 6
console.log(stats.startedAt);       // Date when conversation was created
console.log(stats.lastMessageAt);   // Date of the most recent send()

ConversationStats

PropertyTypeDescription
sessionIdstringCurrent session identifier
userTurnsnumberCount of user messages sent
assistantTurnsnumberCount of assistant replies received
totalMessagesnumberTotal items in the history array
startedAtDateWhen the conversation was created
lastMessageAtDate | nullWhen the last send() completed

Branching with fork()

fork() creates a new Conversation that shares the same history up to that point but has its own session ID. The original conversation is unaffected. This is useful for exploring alternative paths -- for example, comparing different DeFi strategies from the same starting context.

fork-conversation.ts
import { Chaos, Conversation, extractText } from '@chaoslabs/ai-sdk';
 
const chaos = new Chaos({ apiKey: process.env.CHAOS_API_KEY! });
 
const conversation = new Conversation(chaos, {
  userId: 'user-123',
  wallets: [{ address: '0xABC...', chain: 'ethereum' }],
});
 
// Build up shared context
await conversation.send('I have 10 ETH and want to earn yield.');
await conversation.send('What are my options on Aave?');
 
// Fork to explore two strategies independently
const branchA = conversation.fork();
const branchB = conversation.fork();
 
// Each branch diverges from here
const responseA = await branchA.send('Show me the deposit APY for WETH.');
const responseB = await branchB.send('What about supplying USDC instead?');
 
console.log('Branch A:', extractText(responseA));
console.log('Branch B:', extractText(responseB));
 
// Original conversation is unchanged
console.log('Original turns:', conversation.stats.userTurns); // 2

Persistence with toJSON() and fromJSON()

You can serialize a conversation to JSON for storage (database, file, localStorage) and restore it later. This is essential for server-side applications where conversations span multiple HTTP requests.

persistence.ts
import { Chaos, Conversation, extractText } from '@chaoslabs/ai-sdk';
import { writeFileSync, readFileSync } from 'fs';
 
const chaos = new Chaos({ apiKey: process.env.CHAOS_API_KEY! });
 
// --- Save a conversation ---
 
const conversation = new Conversation(chaos, {
  userId: 'user-123',
  wallets: [{ address: '0xABC...', chain: 'ethereum' }],
});
 
await conversation.send('What is the current ETH price?');
await conversation.send('How does that compare to last week?');
 
// Serialize to JSON
const snapshot = conversation.toJSON();
writeFileSync('conversation.json', JSON.stringify(snapshot, null, 2));
 
// --- Restore later ---
 
const saved = JSON.parse(readFileSync('conversation.json', 'utf-8'));
const restored = Conversation.fromJSON(chaos, saved);
 
// Continue where you left off -- full history is available
const response = await restored.send('And what about BTC?');
console.log(extractText(response));
console.log(restored.stats.userTurns); // 3
[@portabletext/react] Unknown block type "callout", specify a component for it in the `components.types` prop

Resetting and Clearing

The Conversation class offers two ways to start over:

  • reset() -- Clears history, resets turn counters, generates a new session ID, and updates the startedAt timestamp. Use this when you want a completely fresh conversation.
  • clearHistory() -- Clears history and resets turn counters but keeps the same session ID. Use this when you want to keep the session identity but discard prior context.
reset-and-clear.ts
// Full reset -- new session ID, fresh start
const oldSessionId = conversation.sessionId;
conversation.reset();
console.log(conversation.stats.totalMessages); // 0
console.log(conversation.sessionId !== oldSessionId); // true
 
// Clear history only -- same session ID
const currentSessionId = conversation.sessionId;
await conversation.send('Hello');
conversation.clearHistory();
console.log(conversation.stats.totalMessages); // 0
console.log(conversation.sessionId === currentSessionId); // true

Accessing the Message History

The messages getter returns a readonly copy of the internal history array. Each item is an InputItem with type, role, and content fields.

message-history.ts
const messages = conversation.messages;
 
for (const msg of messages) {
  console.log(`[${msg.role}]: ${msg.content}`);
}
 
// Example output:
// [user]: What's my portfolio value?
// [assistant]: Your portfolio is worth approximately $45,230...
// [user]: Which asset has the highest allocation?
// [assistant]: ETH represents 62% of your portfolio...

Complete Example: DeFi Research Assistant

Here is an end-to-end example that creates a conversation, conducts a multi-turn research session, forks to explore alternatives, and persists the state.

complete-example.ts
import { Chaos, Conversation, extractText, extractTableBlocks, tableToObjects } from '@chaoslabs/ai-sdk';
import { writeFileSync } from 'fs';
 
async function main() {
  const chaos = new Chaos({ apiKey: process.env.CHAOS_API_KEY! });
 
  const conversation = new Conversation(chaos, {
    userId: 'user-456',
    wallets: [
      { address: '0xMyEthWallet', chain: 'ethereum' },
      { address: '0xMyArbWallet', chain: 'arbitrum' },
    ],
    maxHistoryLength: 30,
  });
 
  // Turn 1: Ask about portfolio
  const r1 = await conversation.send('Show me my portfolio across all chains.');
  console.log(extractText(r1));
 
  // Turn 2: Drill into a specific position
  const r2 = await conversation.send('Tell me more about my Aave positions.');
  const tables = extractTableBlocks(r2);
  if (tables.length > 0) {
    const rows = tableToObjects(tables[0]);
    console.log('Aave positions:', rows);
  }
 
  // Turn 3: Fork to compare two strategies
  const conservativeBranch = conversation.fork();
  const aggressiveBranch = conversation.fork();
 
  const conservative = await conservativeBranch.send(
    'What low-risk yield options do I have for my idle USDC?'
  );
  const aggressive = await aggressiveBranch.send(
    'What are the highest-yield opportunities, even if risky?'
  );
 
  console.log('Conservative:', extractText(conservative));
  console.log('Aggressive:', extractText(aggressive));
 
  // Persist the main conversation for later
  writeFileSync('session.json', JSON.stringify(conversation.toJSON(), null, 2));
 
  // Print final stats
  const stats = conversation.stats;
  console.log(`Session ${stats.sessionId}:`);
  console.log(`  ${stats.userTurns} user turns, ${stats.assistantTurns} assistant turns`);
  console.log(`  Started: ${stats.startedAt.toISOString()}`);
}
 
main().catch(console.error);
Was this helpful?