sqlite CLIYou already built an agent for your travel assistant app, Traivel. Now you will teach it to remember things.
No setup needed; continue with your codebase from Part I.
State lets tools access (read and write) data that persists within a session. Tools access it via the second parameter, the Context object.
Before:
// tools.ts
export const searchDestinations = new FunctionTool({
// ...
execute: ({ region, category, budget }) => fetchDestinations(region, category, budget),
});
After:
import { FunctionTool, type Context } from "@google/adk";
export const searchDestinations = new FunctionTool({
// ...
execute: ({ region, category, budget }, toolContext?: Context) => {
// ... do something with `toolContext`
},
});
Key patterns:
toolContext.state.get — read an object from statetoolContext.state.set — write stateWe are adding two state reads and writes to searchDestinations:
user:preferencestemp:last_destinationCopy this into searchDestinations in tools.ts:
execute: ({ region, category, budget }, toolContext?: Context) => {
// Read saved user preferences
const prefs = toolContext?.state.get<{ region?: string; budget?: string }>("user:preferences");
const effectiveRegion = region ?? prefs?.region;
const effectiveBudget = budget ?? prefs?.budget;
const result = await fetchDestinations(effectiveRegion, category, effectiveBudget);
// Remember last destination viewed
if ("destinations" in result && result.destinations?.[0]) {
toolContext?.state.set("temp:last_destination", result.destinations[0].name);
}
return result;
},
Next we'll add more tools that make use of user-level states.
Add new tools that manage user's wishlist and preferences.
manageWishlistuser:wishlistsavePreferenceuser:preferencesexport const manageWishlist = new FunctionTool({
name: "manage_wishlist",
description:
"Manage the user's travel wishlist. Add or remove destinations, or list the current wishlist. Use this when users express interest in saving or remembering a destination.",
parameters: z.object({
action: z
.enum(["add", "remove", "list"])
.describe("The action to perform: add a destination, remove it, or list the full wishlist."),
destination: z
.string()
.optional()
.describe("The destination name (required for add/remove actions)."),
}),
execute: ({ action, destination }, toolContext?: Context) => {
const wishlist = toolContext?.state.get<string[]>("user:wishlist", []) ?? [];
if (action === "list") {
if (wishlist.length === 0) {
return {
status: "empty",
message: "Your wishlist is empty. Tell me about a destination you'd like to save!",
};
}
return {
status: "success",
wishlist,
message: `You have ${wishlist.length} destination(s) on your wishlist: ${wishlist.join(", ")}.`,
};
}
if (!destination) {
return {
status: "error",
message: "Please specify a destination name for add/remove actions.",
};
}
if (action === "add") {
if (wishlist.includes(destination)) {
return {
status: "already_exists",
message: `${destination} is already on your wishlist!`,
};
}
wishlist.push(destination);
toolContext?.state.set("user:wishlist", wishlist);
return {
status: "added",
wishlist,
message: `Added ${destination} to your wishlist. You now have ${wishlist.length} destination(s).`,
};
}
if (action === "remove") {
const index = wishlist.indexOf(destination);
if (index === -1) {
return {
status: "not_found",
message: `${destination} is not on your wishlist.`,
};
}
wishlist.splice(index, 1);
toolContext?.state.set("user:wishlist", wishlist);
return {
status: "removed",
wishlist,
message: `Removed ${destination} from your wishlist. You now have ${wishlist.length} destination(s).`,
};
}
return { status: "error", message: "Unknown action." };
},
});
export const savePreference = new FunctionTool({
name: "save_preference",
description:
"Save a user's travel preference (budget level or region preference) so future searches use it as default. Call this when the user explicitly states a preference.",
parameters: z.object({
preferred_region: z
.enum(["domestic", "international"])
.optional()
.describe("The user's preferred travel region."),
preferred_budget: z
.enum(["budget", "mid-range", "luxury"])
.optional()
.describe("The user's preferred budget level."),
}),
execute: ({ preferred_region, preferred_budget }, toolContext?: Context) => {
const prefs = toolContext?.state.get<{ region?: string; budget?: string }>("user:preferences") ?? {};
const saved: string[] = [];
if (preferred_region) {
prefs.region = preferred_region;
saved.push(`region: ${preferred_region}`);
}
if (preferred_budget) {
prefs.budget = preferred_budget;
saved.push(`budget: ${preferred_budget}`);
}
if (saved.length === 0) {
return {
status: "error",
message: "No preference provided. Specify preferred_region or preferred_budget.",
};
}
toolContext?.state.set("user:preferences", prefs);
return {
status: "saved",
message: `Preferences saved: ${saved.join(", ")}. Future searches will use these as defaults.`,
};
},
});
What did you notice about the prefixes?
State keys use prefixes to control scope and persistence:
Prefix | Scope | Persisted with DB? |
| Per user, all sessions | Yes |
| All users, all sessions | Yes |
| Current invocation only | No, always ephemeral |
(none) | Per session only | Yes |
user:wishlist survives across sessions (with a database), while temp:last_destination is gone after each call.
ADK includes a ready-to-use GOOGLE_SEARCH tool. It lets the agent fetch real-time info via the Gemini API. No extra configuration needed.
Add this to the root agent:
// agent.ts
import {
// ... other imports,
GOOGLE_SEARCH,
} from "@google/adk";
export const rootAgent = new LlmAgent({
// ... name, model, description, instruction, tools
tools: [
// ... other tools
GOOGLE_SEARCH,
],
});
The agent decides when to use it. If the user asks about a destination, search_destinations handles it. If they ask about current conditions — weather, events, prices — the agent uses google_search.
Update the agent instruction to tell it about this capability:
- **Real-time info**: Use \`google_search\` when users ask about current weather, events, prices, or anything that needs up-to-date information.
Test:
npx adk run agent.ts
google_searchSo far, sessions live in memory. It's gone when the process stops. Let's persist them to SQLite DB.
The ADK CLI accepts a --session_service_uri flag:
# In-memory sessions (default — lost on restart)
npx adk web
# Persistent sessions with SQLite
npx adk web --session_service_uri "sqlite://./traivel.db"
A traivel.db file appears in your project directory.
npx adk web --session_service_uri "sqlite://./traivel.db"user:wishlistuser:preferencesADK creates 5 tables:
Table | Stores |
| Schema version |
|
|
|
|
| Session-level state |
| Every event — messages, responses, tool calls |
State keys are split by prefix into separate tables. user:preferences lands in user_states.state — stored without the user: prefix. temp: keys are never persisted.
# List all tables
sqlite3 traivel.db ".tables"
# User state (preferences, wishlist)
sqlite3 traivel.db "SELECT user_id, state FROM user_states;"
# Session state + metadata
sqlite3 traivel.db "SELECT id, user_id, state FROM sessions;"
# Event count per session
sqlite3 traivel.db "SELECT session_id, COUNT(*) FROM events GROUP BY session_id;"
Persistent state stores values you explicitly set. Memory goes further — it lets the agent search across past conversation events for relevant context.
The CLI has a --session_service_uri flag for persistent sessions, but no flag for memory. To wire up InMemoryMemoryService, use a programmatic Runner.
Create runner.ts:
import { Runner, DatabaseSessionService, InMemoryMemoryService } from "@google/adk";
import { rootAgent } from "./agent";
const APP_NAME = "traivel";
// In production, get this from DB e.g. based on auth cookie.
const MOCK_USER_ID = "johndoe123";
async function main() {
// --- 1. Initiate session service --- //
const sessionService = new DatabaseSessionService("sqlite://./traivel.db");
await sessionService.init();
// --- 2. Initiate memory service (in memory; lost on server restart) --- //
const memoryService = new InMemoryMemoryService();
// --- 3. Create the Runner --- //
const runner = new Runner({
agent: rootAgent,
appName: APP_NAME,
sessionService,
memoryService,
});
// --- 4. Run a conversation turn --- //
const session = await sessionService.createSession({
appName: APP_NAME,
userId: MOCK_USER_ID,
});
const events = runner.runAsync({
userId: MOCK_USER_ID,
sessionId: session.id,
// Sample user question
newMessage: { role: "user", parts: [{ text: "Recommend a budget beach destination" }] },
});
for await (const event of events) {
if (event.content?.parts?.[0]?.text) {
console.log(`Agent: ${event.content.parts[0].text}`);
}
}
// --- 5. Write session to memory --- //
const fullSession = await sessionService.getSession({
appName: APP_NAME,
userId: MOCK_USER_ID,
sessionId: session.id,
});
if (fullSession) {
await memoryService.addSessionToMemory(fullSession);
}
// --- 6. Read from memory --- //
const memories = await memoryService.searchMemory({
appName: APP_NAME,
userId: MOCK_USER_ID,
query: "beach", // Search query based on sample data
});
console.log(`✓ Memory search found ${memories.memories.length} match(es)`);
}
main().catch(console.error);
After a session ends, call memoryService.addSessionToMemory(session) to index it. In future sessions, the agent can search past events using keyword matching.
For example: if a user mentioned "I love diving" in Session 1, the agent can retrieve that in Session 3 when recommending destinations — even though it's a different session.
ADK provides a built-in PRELOAD_MEMORY tool. It searches memory using the user's query and injects matching past events as context. Add it to the agent's tools:
import { PRELOAD_MEMORY } from "@google/adk";
export const rootAgent = new LlmAgent({
// ...
tools: [
searchDestinations, checkVisaRequirement,
manageWishlist, savePreference,
skillToolset, PRELOAD_MEMORY,
],
});
Without PRELOAD_MEMORY, the memory service is configured and addSessionToMemory() works, but no agent turn retrieves memories. Adding PRELOAD_MEMORY without a memoryService on the Runner is a no-op — it silently skips the lookup.
Update the instruction to reference past conversations:
Memory:
- If a memoryService is configured, previous conversations provide context about the user's travel preferences.
- Reference past discussions when relevant (e.g., "Last time you were interested in Bali...").
Session | Memory | |
Scope | One conversation | Across conversations |
What's stored | Live state + event stream | Past events (keyword-indexed) |
Access pattern |
|
|
npm run runner
This runs a single conversation turn and logs events. Check the database:
sqlite3 traivel.db "SELECT session_id, COUNT(*) FROM events GROUP BY session_id;"
Run the agent as a local REST API server:
# In-memory sessions
npx adk api_server
# With persistent sessions
npx adk api_server --session_service_uri "sqlite://./traivel.db"
This starts an HTTP server at http://localhost:8000 with endpoints for session management and querying.
curl -X POST \
http://localhost:8000/apps/traivel_agent/users/learner/sessions \
-H "Content-Type: application/json" \
-d '{}'
Response includes a session_id. Use it in the next calls.
The JS API server uses /run (not /query like the Python version):
curl -X POST http://localhost:8000/run \
-H "Content-Type: application/json" \
-d '{
"appName": "traivel_agent",
"userId": "learner",
"sessionId": "{SESSION_ID}",
"newMessage": {
"role": "user",
"parts": [{"text": "Recommend a budget beach destination"}]
}
}'
curl http://localhost:8000/apps/traivel_agent/users/learner/sessions/{SESSION_ID}
The response includes a state field with all stored values.
curl -X POST http://localhost:8000/run \
-H "Content-Type: application/json" \
-d '{
"appName": "traivel_agent",
"userId": "learner",
"sessionId": "{SESSION_ID}",
"newMessage": {
"role": "user",
"parts": [{"text": "Add Bali to my wishlist"}]
}
}'
ADK can containerize and deploy the API server to Google Cloud Run:
adk deploy cloud_run .
This generates a Dockerfile running adk api_server, bundles your agent files, and runs gcloud run deploy. The result is a public REST endpoint — the same API you tested locally, just containerized.
You added state, persistence, and memory to Traivel.
Feature | API | Scope |
Session state (get/set) |
| Per-session (or persistent with DB) |
User preferences |
| Survives across sessions (with DB) |
Session temp data |
| Current invocation only |
App-wide data |
| Shared across all users |
Persistent sessions |
| SQLite file on disk |
Cross-session memory |
| Keyword search over past events |
Google Search grounding |
| Real-time info via Gemini API |
REST API |
| HTTP testing |
File | Changes |
| State reads/writes, |
| New tools, state-aware instructions |
| (new) Programmatic Runner with DB + Memory |
| New scripts ( |
Optional: Part III shows how this agent is added to a Next.js web app with a custom chat UI.