Skip to main content

Overview

Database sessions provide robust state management and persistence for production agents. This example demonstrates:
  • Persisting state across application restarts
  • Storing conversation history in a database
  • Managing artifacts (generated files)
  • Event compaction for long conversations
  • Time-travel with session rewind
While in-memory sessions (withQuickSession) are great for development, database sessions are essential for production applications where state must survive restarts and scale across multiple instances.

Counter Application Example

We’ll build a stateful counter application that demonstrates:
  1. Persistence: Counter values stored in SQLite
  2. Artifacts: Saving reports to files
  3. Rewind: Undoing state changes
  4. Compaction: Summarizing long conversation histories

Complete Example

1
Step 1: Create Stateful Tools
2
Tools that modify database-persisted state:
3
import { createTool } from "@iqai/adk";
import z from "zod";

export const counterTool = createTool({
	name: "increment_counter",
	description: "Increment a named counter",
	schema: z.object({
		counterName: z.string().describe("Name of the counter"),
		amount: z.number().default(1).describe("Amount to increment"),
	}),
	fn: ({ counterName, amount }, context) => {
		const counters = context.state.get("counters", {});
		const oldValue = counters[counterName] || 0;
		const newValue = oldValue + amount;
		const newCounters = { ...counters, [counterName]: newValue };
		context.state.set("counters", newCounters);
		return {
			counterName,
			oldValue,
			newValue,
			increment: amount,
		};
	},
});

export const saveCounterReportTool = createTool({
	name: "save_counter_report",
	description: "Save a report of all counter values to an artifact file",
	schema: z.object({
		filename: z.string().describe("Name of the file to save the report to"),
	}),
	fn: async ({ filename }, context) => {
		const counters = context.state.get("counters", {});
		const report = Object.entries(counters)
			.map(([name, value]) => `${name}: ${value}`)
			.join("\n");

		await context.saveArtifact(filename, {
			text: report || "No counters found",
		});

		return {
			success: true,
			filename,
			countersCount: Object.keys(counters).length,
		};
	},
});
4
The context.saveArtifact() method stores files that are separate from the conversation history. Artifacts are perfect for generated reports, images, or any file output.
5
Step 2: Configure Database Session Service
6
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import {
	AgentBuilder,
	createDatabaseSessionService,
	InMemoryArtifactService,
} from "@iqai/adk";
import { counterTool, saveCounterReportTool } from "./tools";

export async function getRootAgent() {
	const dbDir = path.join(os.tmpdir(), "adk-examples");
	if (!fs.existsSync(dbDir)) {
		fs.mkdirSync(dbDir, { recursive: true });
	}

	const sessionService = createDatabaseSessionService(
		getSqliteConnectionString("sessions"),
	);
	const artifactService = new InMemoryArtifactService();

	return await AgentBuilder.withModel(
		process.env.LLM_MODEL || "gemini-3-flash-preview",
	)
		.withTools(counterTool, saveCounterReportTool)
		.withSessionService(sessionService)
		.withArtifactService(artifactService)
		.withEventsCompaction({
			compactionInterval: 3,
			overlapSize: 1,
		})
		.build();
}

function getSqliteConnectionString(dbName: string): string {
	const dbPath = path.join(os.tmpdir(), "adk-examples", `${dbName}.db`);
	return `sqlite://${dbPath}`;
}
7
The createDatabaseSessionService function supports both SQLite (sqlite://path) and PostgreSQL (postgresql://...) connection strings.
8
Step 3: Demonstrate All Features
9
import { BaseSessionService, Session } from "@iqai/adk";
import { ask } from "../utils";
import { getRootAgent } from "./agents/agent";

async function main() {
	const { runner, sessionService, session } = await getRootAgent();

	// 1. Persistence: Counter stored in SQLite database
	console.log("\n📊 Persistence: Counter stored in DB...");
	await ask(runner, "Increment 'visits' counter by 1 and show its value");

	// 2. Artifacts: Save counter report to file
	console.log("\n📁 Artifacts: Saving counter report...");
	await ask(runner, 'Save current counter values to "counter-report.txt"');

	// 3. Rewind: Undo the last counter increment
	console.log("\n🔄 Rewind: Demonstrating time-travel...");

	// Show current state
	let currentSession = await sessionService.getSession(
		session.appName,
		session.userId,
		session.id,
	);
	console.log("📍 State before big increment:", currentSession.state);

	// Make a big increment that we'll want to undo
	await ask(runner, "Increment 'visits' counter by 100");

	// Get the invocationId of the increment operation
	const invocationId = await getInvocationIdWithStateDelta(
		sessionService,
		session,
	);

	// Show the state after the big increment
	currentSession = await sessionService.getSession(
		session.appName,
		session.userId,
		session.id,
	);
	console.log("📍 State after +100 increment:", currentSession.state);

	// Rewind to before the +100 increment - undoing that change!
	if (invocationId) {
		await runner.rewind({
			userId: session.userId,
			sessionId: session.id,
			rewindBeforeInvocationId: invocationId,
		});

		// Show the state after rewind - it should be back to what it was before
		currentSession = await sessionService.getSession(
			session.appName,
			session.userId,
			session.id,
		);
		console.log("⏪ State after rewind:", currentSession.state);
		console.log("✨ Successfully rewound - the +100 never happened!");

		// Verify with the agent (it will use the restored state)
		await ask(runner, "Increment 'visits' by 1 and show the new value");
	}

	// 4. Compaction: Multiple counter ops trigger auto-summarization
	console.log("\n📦 Compaction: Multiple counter operations...");
	const counters = ["logins", "clicks", "views", "shares"];
	for (const counter of counters) {
		await runner.ask(`Increment '${counter}' counter by 1`);
		await logCompactions(sessionService, session);
	}
	await ask(runner, "Show all counters");

	console.log("\n✅ All features demonstrated with counters!");
}

/**
 * Gets the invocationId of the last event that has a state delta.
 */
async function getInvocationIdWithStateDelta(
	sessionService: BaseSessionService,
	session: Session,
): Promise<string | undefined> {
	const currentSession = await sessionService.getSession(
		session.appName,
		session.userId,
		session.id,
	);

	// Search backwards through events to find the last state-changing event
	for (let i = currentSession.events.length - 1; i >= 0; i--) {
		const event = currentSession.events[i];
		if (
			event.actions?.stateDelta &&
			Object.keys(event.actions.stateDelta).length > 0
		) {
			return event.invocationId;
		}
	}

	return undefined;
}

async function logCompactions(
	sessionService: BaseSessionService,
	session: Session,
) {
	const updatedSession = await sessionService.getSession(
		session.appName,
		session.userId,
		session.id,
	);

	const compactions = updatedSession.events
		.filter((e) => e.actions?.compaction)
		.map((e) => e.actions.compaction);

	if (compactions.length === 0) return;

	for (const [i, c] of compactions.entries()) {
		const parts = c.compactedContent?.parts ?? [];
		const text = parts.map((p: any) => p.text).join("\n");
		console.log(`📦 Compaction ${i + 1}: ${text.substring(0, 100)}...\n`);
	}
}

main().catch(console.error);
10
Step 4: Run the Example
11
node index.ts

Expected Output

📊 Persistence: Counter stored in DB...
👤 User: Increment 'visits' counter by 1 and show its value
🤖 Agent: I've incremented the 'visits' counter by 1. The new value is 1.

📁 Artifacts: Saving counter report...
👤 User: Save current counter values to "counter-report.txt"
🤖 Agent: I've saved the counter report to counter-report.txt. The file contains 1 counter.

🔄 Rewind: Demonstrating time-travel...
📍 State before big increment: { counters: { visits: 1 } }

👤 User: Increment 'visits' counter by 100
🤖 Agent: Incremented 'visits' by 100. New value: 101

📍 State after +100 increment: { counters: { visits: 101 } }
⏪ State after rewind: { counters: { visits: 1 } }
✨ Successfully rewound - the +100 never happened!

👤 User: Increment 'visits' by 1 and show the new value
🤖 Agent: Incremented 'visits' by 1. New value: 2

📦 Compaction: Multiple counter operations...
📦 Compaction 1: User incremented multiple counters: logins (1), clicks (1), views (1)...

✅ All features demonstrated with counters!

Key Concepts

Database vs In-Memory Sessions

import { AgentBuilder } from "@iqai/adk";

const agent = AgentBuilder.create("my_agent")
	.withQuickSession({ state: { count: 0 } })
	.build();

// ✅ Fast, simple
// ❌ Lost on restart
// ❌ Not shared across instances

Event Compaction

Long conversations are automatically summarized to save tokens:
.withEventsCompaction({
	compactionInterval: 3,  // Compact every 3 interactions
	overlapSize: 1,         // Keep 1 recent message for context
})
How it works:
  1. After N interactions, older messages are summarized
  2. Summaries replace original messages in the context
  3. State changes are preserved
  4. Recent messages remain intact for continuity

Session Rewind

Undo state changes by rewinding to a previous point:
await runner.rewind({
	userId: session.userId,
	sessionId: session.id,
	rewindBeforeInvocationId: invocationId,
});
Use cases:
  • Undo incorrect tool executions
  • Handle user “cancel” requests
  • Recover from errors
  • A/B test different paths

Artifact Management

Artifacts store generated files separately from conversation history:
// Save artifact in a tool
await context.saveArtifact("report.pdf", {
	binary: pdfBuffer,
	contentType: "application/pdf",
});

// Retrieve artifact later
const artifact = await artifactService.getArtifact(sessionId, "report.pdf");

Production Setup

PostgreSQL Connection

For production, use PostgreSQL instead of SQLite:
import { createDatabaseSessionService } from "@iqai/adk";

const sessionService = createDatabaseSessionService(
	"postgresql://user:password@localhost:5432/adk_sessions",
);

Connection Pooling

const sessionService = createDatabaseSessionService(
	"postgresql://user:password@localhost:5432/adk_sessions",
	{
		pool: {
			min: 2,
			max: 10,
			idleTimeoutMillis: 30000,
		},
	}
);

File-based Artifacts

For production, store artifacts on disk or cloud storage:
import { FileArtifactService } from "@iqai/adk";
import * as path from "node:path";

const artifactService = new FileArtifactService({
	basePath: path.join(__dirname, "artifacts"),
});

// Or use S3, GCS, etc.
import { S3ArtifactService } from "@iqai/adk";

const artifactService = new S3ArtifactService({
	bucket: "my-app-artifacts",
	region: "us-west-2",
});

Session Cleanup

Automatically delete old sessions:
import { createDatabaseSessionService } from "@iqai/adk";

const sessionService = createDatabaseSessionService(
	"postgresql://...",
	{
		cleanup: {
			enabled: true,
			retentionDays: 30, // Delete sessions older than 30 days
			intervalHours: 24, // Run cleanup daily
		},
	}
);

Advanced Patterns

Multi-User Sessions

// Create session for specific user
const session = await sessionService.createSession(
	"my-app",      // appName
	"user-123",    // userId
);

runner.setSession(session);

// Later: Resume the same session
const sessions = await sessionService.listSessions("my-app", "user-123");
const lastSession = sessions[0];
runner.setSession(lastSession);

Session Metadata

const session = await sessionService.createSession(
	"my-app",
	"user-123",
	{
		metadata: {
			device: "mobile",
			version: "2.0.1",
			location: "US-West",
		},
	}
);

// Query sessions by metadata
const mobileSessions = await sessionService.findSessions({
	appName: "my-app",
	metadata: { device: "mobile" },
});

Concurrent Session Updates

// Use optimistic locking for concurrent updates
try {
	await sessionService.updateSession(session, {
		state: newState,
		expectedVersion: session.version,
	});
} catch (error) {
	if (error.code === "VERSION_MISMATCH") {
		// Reload session and retry
		const latestSession = await sessionService.getSession(
			session.appName,
			session.userId,
			session.id,
		);
		// Merge changes and retry...
	}
}

Performance Optimization

Lazy Loading Events

// Load only recent events
const session = await sessionService.getSession(
	session.appName,
	session.userId,
	session.id,
	{ maxEvents: 50 } // Only load last 50 events
);

Batch Operations

// Update multiple sessions efficiently
await sessionService.batchUpdate([
	{ sessionId: "sess-1", state: { count: 1 } },
	{ sessionId: "sess-2", state: { count: 2 } },
]);

Use Cases

Multi-Session Apps

Resume conversations across devices and sessions

Collaboration Tools

Multiple users interacting with the same agent

Long-Running Tasks

Workflows that span hours or days

Audit & Compliance

Full conversation history for regulatory needs

Next Steps

Memory Systems

Add semantic memory to persistent sessions

Multi-Agent Systems

Coordinate agents with shared persistent state

Source Code

View the complete example in the repository: apps/examples/src/04-persistence-and-sessions

Build docs developers (and LLMs) love