TypeScriptADK-TS

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 instructionsReadonlyContext
Read session state in a callbackReadonlyContext
Write to state or save an artifactCallbackContext
Search memory or list artifactsToolContext
Access services in a custom agentInvocationContext

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.

PrefixLifetimeExample 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