TypeScriptADK-TS

Best Practices

Caching, batching, validation, cleanup, and naming conventions for artifact management in ADK-TS.

This page covers patterns that prevent common mistakes when working with artifacts at scale — from avoiding unnecessary service calls to safely cleaning up files after processing.

Cache frequently read artifacts

Every loadArtifact call hits the backing service. For GCS this means a network round-trip on every callback or tool invocation. If the same artifact is read many times per session and doesn't change often, wrap it in an in-process cache with a TTL:

import type { BaseArtifactService } from "@iqai/adk";
import type { Part } from "@google/genai";

const cache = new Map<string, { part: Part; ts: number }>();
const TTL_MS = 5 * 60 * 1000; // 5 minutes

async function loadWithCache(
  svc: BaseArtifactService,
  key: {
    appName: string;
    userId: string;
    sessionId: string;
    filename: string;
    version?: number;
  },
): Promise<Part | null> {
  const k = `${key.appName}:${key.userId}:${key.sessionId}:${key.filename}:${key.version ?? "latest"}`;
  const hit = cache.get(k);
  if (hit && Date.now() - hit.ts < TTL_MS) return hit.part;

  const part = await svc.loadArtifact(key);
  if (part) cache.set(k, { part, ts: Date.now() });
  return part;
}

This is most useful with GcsArtifactService. InMemoryArtifactService has no network overhead and doesn't need caching.

Batch saves with Promise.all

Awaiting saves one at a time blocks each call until the previous one completes. When you need to save multiple artifacts in a single callback or tool, run them in parallel with Promise.all:

// ❌ Sequential — each save blocks the next
await ctx.saveArtifact("summary.txt", { text: "..." });
await ctx.saveArtifact("metadata.json", {
  inlineData: { data: metaB64, mimeType: "application/json" },
});
await ctx.saveArtifact("chart.png", {
  inlineData: { data: chartB64, mimeType: "image/png" },
});

// ✅ Parallel — all three saves run at the same time
await Promise.all([
  ctx.saveArtifact("summary.txt", { text: "..." }),
  ctx.saveArtifact("metadata.json", {
    inlineData: { data: metaB64, mimeType: "application/json" },
  }),
  ctx.saveArtifact("chart.png", {
    inlineData: { data: chartB64, mimeType: "image/png" },
  }),
]);

Validate before saving

When the content or filename comes from user input, validate it before calling saveArtifact. Without checks, a user can pass unsupported MIME types, very large files that exhaust memory or storage, or filenames with special characters that break downstream tooling.

import type { Part } from "@google/genai";

const ALLOWED_MIME = new Set([
  "text/plain",
  "text/csv",
  "application/json",
  "image/png",
  "image/jpeg",
  "application/pdf",
]);
const MAX_BYTES = 50 * 1024 * 1024; // 50 MB

function validatePart(
  filename: string,
  part: Part,
): { ok: boolean; error?: string } {
  // Only allow safe filename characters (strip user: prefix before checking)
  if (!/^[a-zA-Z0-9._-]+$/.test(filename.replace("user:", "")))
    return { ok: false, error: "Invalid filename characters" };

  // text and fileData parts have no binary payload to size-check
  if (!part.inlineData) return { ok: true };

  const { mimeType, data } = part.inlineData;
  if (!ALLOWED_MIME.has(mimeType))
    return { ok: false, error: `Unsupported MIME type: ${mimeType}` };

  const size = Buffer.from(data, "base64").length;
  if (size > MAX_BYTES)
    return {
      ok: false,
      error: `File too large: ${size} bytes (max ${MAX_BYTES})`,
    };

  return { ok: true };
}

Naming conventions

Consistent filename prefixes make it easy to list, filter, and clean up related artifacts programmatically:

PrefixScopeExampleNotes
(none)Session"report.pdf", "summary.txt"Default — discarded on session end
user:User"user:avatar.png", "user:settings.json"Persists across sessions
temp_Session"temp_upload.csv", "temp_parse_result.json"Delete immediately after use
cache_Either"cache_analysis.json"Safe to delete and regenerate

Cleanup and deletion

deleteArtifact removes all versions of a filename permanently and is only available on the service directly — not on context objects.

Deletion is permanent and removes all versions. There is no undo. Test cleanup logic in a staging environment before running in production.

To delete a single file after processing:

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

const svc = new GcsArtifactService("my-bucket");

await svc.deleteArtifact({
  appName: "my_app",
  userId: "u1",
  sessionId: "s1",
  filename: "temp_upload.csv",
});

To delete all artifacts matching a prefix — for example, cleaning up all temp_ files after a session:

import type { BaseArtifactService } from "@iqai/adk";

async function deleteByPrefix(
  svc: BaseArtifactService,
  args: { appName: string; userId: string; sessionId: string },
  prefix: string,
) {
  const keys = await svc.listArtifactKeys(args);
  await Promise.all(
    keys
      .filter(k => k.startsWith(prefix))
      .map(filename => svc.deleteArtifact({ ...args, filename })),
  );
}

// Example: delete all temp_ artifacts for a session
await deleteByPrefix(
  svc,
  { appName: "my_app", userId: "u1", sessionId: "s1" },
  "temp_",
);

For GCS, you can also configure a bucket lifecycle policy to auto-delete blobs older than a set number of days — useful for enforcing data retention limits without manual cleanup:

{
  "lifecycle": {
    "rule": [{ "action": { "type": "Delete" }, "condition": { "age": 90 } }]
  }
}
gsutil lifecycle set lifecycle.json gs://my-artifacts-bucket

Retry transient errors

GCS calls can fail transiently due to network blips or temporary service unavailability. Rather than letting a single failure crash a callback, wrap GCS operations in exponential backoff. The delay doubles on each attempt, giving the service time to recover:

async function withRetry<T>(
  op: () => Promise<T>,
  attempts = 3,
  baseDelayMs = 1000,
): Promise<T> {
  let lastError: unknown;
  for (let i = 0; i <= attempts; i++) {
    try {
      return await op();
    } catch (e) {
      lastError = e;
      if (i < attempts) {
        await new Promise(r => setTimeout(r, baseDelayMs * 2 ** i));
      }
    }
  }
  throw lastError;
}

// Usage
const artifact = await withRetry(() =>
  svc.loadArtifact({
    appName: "my_app",
    userId: "u1",
    sessionId: "s1",
    filename: "report.pdf",
  }),
);

This approach works for any artifact service call — saveArtifact, loadArtifact, deleteArtifact, or listArtifactKeys.

Next steps