Context Patterns
State scoping, context selection, anti-patterns, and testing strategies for ADK-TS context objects.
A few recurring patterns come up whenever you work with context objects. This page collects the ones that save the most time.
Pick the right context type
Use the weakest context type that satisfies your requirements. Callbacks and functions that receive a powerful context but only read state are harder to reason about — a ReadonlyContext parameter makes the intent clear and prevents accidental mutations.
| You need to… | Use |
|---|---|
| Generate dynamic instructions | ReadonlyContext |
| Read session state in a callback | ReadonlyContext |
| Write to state or save an artifact | CallbackContext |
| Search memory or list artifacts | ToolContext |
| Access services in a custom agent | InvocationContext |
State scoping
ADK-TS uses key prefixes to control how long a state value lives. Using the wrong prefix is a common source of values disappearing unexpectedly or persisting longer than intended.
| Prefix | Lifetime | Example key |
|---|---|---|
| (none) | Current session | "taskStatus" |
user: | Across all sessions for this user | "user:name" |
app: | Shared across all users | "app:featureFlags" |
temp: | Current turn only | "temp:draftReply" |
import { CallbackContext } from "@iqai/adk";
function applyStateScoping(ctx: CallbackContext) {
// Persists for the life of this session
ctx.state["currentTask"] = "onboarding";
// Persists across all sessions for this user
ctx.state["user:name"] = "Alice";
// Shared across every user in the app
ctx.state["app:maintenanceMode"] = false;
// Cleared after the current agent turn
ctx.state["temp:searchResults"] = [];
}The prefixes use a colon (:) not a dot (.). "user:name" is the correct
form; "user.name" is treated as a plain session-scoped key.
Common anti-patterns
Using a more powerful context than needed
// ❌ ToolContext just to read a state value
function getPreference(ctx: ToolContext): string {
return ctx.state["user:theme"];
}
// ✅ ReadonlyContext is sufficient for reads
function getPreference(ctx: ReadonlyContext): string {
return ctx.state["user:theme"] as string;
}Storing context references across calls
Context objects are valid only for the duration of a single callback or tool execution. Holding onto a reference after the call returns leads to stale state and undefined behaviour.
// ❌ Storing context on a service instance
class BadService {
private ctx: ToolContext; // Don't do this
setContext(ctx: ToolContext) {
this.ctx = ctx;
}
}
// ✅ Pass context to each operation directly
class GoodService {
async search(query: string, ctx: ToolContext) {
return ctx.searchMemory(query);
}
}Ignoring service availability
artifactService and memoryService are optional. Calling methods on them without checking throws at runtime.
// ❌ Will throw if memory service is not configured
async function badSearch(ctx: ToolContext, query: string) {
return await ctx.searchMemory(query);
}
// ✅ Guard or use try/catch
async function safeSearch(ctx: ToolContext, query: string) {
try {
return await ctx.searchMemory(query);
} catch {
return [];
}
}Testing
The simplest approach is to pass plain objects that satisfy the type. For ReadonlyContext, only a few properties are needed for most tests:
import type { ReadonlyContext } from "@iqai/adk";
function buildInstruction(ctx: ReadonlyContext): string {
const name = ctx.state["user:name"] ?? "there";
return `Hello, ${name}!`;
}
// Unit test — no framework setup required
test("builds instruction with user name", () => {
const ctx = {
userContent: undefined,
invocationId: "test-inv-001",
agentName: "greeter",
appName: "my-app",
userId: "user-1",
sessionId: "session-1",
state: Object.freeze({ "user:name": "Alice" }),
} as ReadonlyContext;
expect(buildInstruction(ctx)).toBe("Hello, Alice!");
});For ToolContext in Vitest, stub only the methods your tool actually calls:
import { vi } from "vitest";
import type { ToolContext, MemorySearchResult } from "@iqai/adk";
function buildMockToolContext(
stateValues: Record<string, unknown> = {},
): ToolContext {
const state: Record<string, unknown> = { ...stateValues };
return {
invocationId: "test-inv-001",
agentName: "test-agent",
appName: "test-app",
userId: "user-1",
sessionId: "session-1",
userContent: undefined,
functionCallId: "call-001",
state: new Proxy(state, {
get: (t, k) => {
if (k === "get") return (key: string) => t[key];
if (k === "set")
return (key: string, val: unknown) => {
t[key] = val;
};
return t[k as string];
},
set: (t, k, v) => {
t[k as string] = v;
return true;
},
}) as any,
actions: { stateDelta: {}, artifactDelta: {} } as any,
searchMemory: vi
.fn<[string], Promise<MemorySearchResult[]>>()
.mockResolvedValue([]),
listArtifacts: vi.fn().mockResolvedValue([]),
loadArtifact: vi.fn().mockResolvedValue(undefined),
saveArtifact: vi.fn().mockResolvedValue(0),
} as unknown as ToolContext;
}Next steps
InvocationContext
The internal execution environment for a single agent invocation — exposes all services, session data, and lifecycle controls.
Context Caching
A guide explaining how to configure and use context caching in ADK-TS with Gemini and Anthropic models to reuse large prompts efficiently, reducing latency and token costs.