Skip to content

GCP Cloud Functions

Google Cloud Functions (2nd gen) is the simplest deployment option for ModernPath agents. The framework provides the gcpHttp() adapter that wraps your handler into the Express-compatible (req, res) => void signature expected by the GCP Functions Framework.


How It Works

The gcpHttp() adapter performs three steps:

  1. Converts the Express req object into a platform-neutral HttpEvent.
  2. Passes the event to your framework handler (createAgentExecuteHandler or createAgentExecuteStreamHandler).
  3. Writes the JsonResponse back to the Express res object.
sequenceDiagram
    participant CF as Cloud Function
    participant Adapter as gcpHttp()
    participant Handler as createAgentExecuteHandler
    participant Agent as Your Agent

    CF->>Adapter: req, res
    Adapter->>Handler: HttpEvent
    Handler->>Agent: AgentContext
    Agent-->>Handler: AgentResult
    Handler-->>Adapter: JsonResponse
    Adapter-->>CF: res.status().send()

Entry Point

Basic (JSON response)

src/index.ts
import {
  createAgentExecuteHandler,
  gcpHttp,
  GeminiClient,
  PromptTemplate,
  ToolRegistry,
} from "@modernpath/agent-framework";

import { MyAgent } from "./agents/MyAgent";

// Build dependencies once (reused across invocations)
const gemini = new GeminiClient({
  apiKey: process.env.GOOGLE_AI_STUDIO_KEY!,
  model: process.env.GEMINI_MODEL || "gemini-3-flash-preview",
});

const tools = new ToolRegistry();
const agent = new MyAgent(tools, { gemini });

// Framework handler
const handler = createAgentExecuteHandler({
  resolveAgent: (agentType) => {
    if (agentType === "my-agent") return agent;
    throw new Error(`Unknown agentType: ${agentType}`);
  },
  getUserId: async (event) => {
    // Extract user ID from auth header, token, etc.
    return 1;
  },
  cors: { origin: "*" },
});

// Export the Cloud Function entry point
export const api = gcpHttp(handler);

Streaming (SSE)

For streaming responses (used by AgentChat with the SSE backend), use the dedicated stream endpoint factory:

src/index.ts
import {
  createGcpAgentExecuteStreamEndpoint,
} from "@modernpath/agent-framework";

export const apiStream = createGcpAgentExecuteStreamEndpoint({
  resolveAgent: (agentType) => {
    if (agentType === "my-agent") return agent;
    throw new Error(`Unknown agentType: ${agentType}`);
  },
  getUserId: async () => 1,
  cors: { origin: "*" },
});

createGcpAgentExecuteStreamEndpoint

This convenience factory composes createAgentExecuteStreamHandler with the GCP SSE adapter internally. It handles writing SSE chunks to the Express response and calling res.end() when the stream completes.

Combined Entry Point

In practice, you often need a single Cloud Function that handles multiple routes (execute, stream, health, admin). Use a router pattern:

src/index.ts
import {
  createAgentExecuteHandler,
  createAgentExecuteStreamHandler,
  gcpHttp,
} from "@modernpath/agent-framework";

// ... (build agent, tools, gemini as above)

const executeHandler = createAgentExecuteHandler({ resolveAgent, getUserId, cors });
const streamHandler = createAgentExecuteStreamHandler({ resolveAgent, getUserId, cors });

export const api = async (req: any, res: any) => {
  // Set CORS headers
  res.set("access-control-allow-origin", "*");
  res.set("access-control-allow-headers", "content-type, authorization");
  res.set("access-control-allow-methods", "POST, OPTIONS");
  if (req.method === "OPTIONS") return res.status(204).send("");

  const pathname = String(req.path || "/");

  // Health check
  if (pathname === "/health") {
    return res.status(200).send(JSON.stringify({ ok: true }));
  }

  // Route: /api/agents/:auditingId/execute/stream
  if (pathname.includes("/execute/stream")) {
    // ... wire streamHandler with SSE writing
  }

  // Route: /api/agents/:auditingId/execute
  if (pathname.includes("/execute")) {
    const event = toHttpEvent(req); // (1)
    const result = await executeHandler(event);
    res.status(result.statusCode);
    for (const [k, v] of Object.entries(result.headers || {})) res.set(k, v);
    return res.send(result.body);
  }

  return res.status(404).send(JSON.stringify({ error: "Not found" }));
};
  1. toHttpEvent converts the Express request into the framework's HttpEvent type. See the Handler Factories reference for the full interface.

Bundling with esbuild

Cloud Functions 2nd gen supports ESM modules. Use esbuild to produce a self-contained bundle:

package.json
{
  "scripts": {
    "build:gcp": "rm -rf dist-gcp && mkdir -p dist-gcp && esbuild src/index.ts --bundle --platform=node --target=node20 --format=esm --outfile=dist-gcp/index.mjs --external:@google-cloud/firestore && cp package.gcp.json dist-gcp/package.json"
  },
  "devDependencies": {
    "esbuild": "^0.25.0"
  }
}
package.json
{
  "scripts": {
    "build:gcp": "rm -rf dist-gcp && mkdir -p dist-gcp && esbuild src/index.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist-gcp/index.cjs --external:@google-cloud/firestore && cp package.gcp.json dist-gcp/package.json"
  },
  "devDependencies": {
    "esbuild": "^0.25.0"
  }
}

Create a minimal package.json for the deployment artifact:

package.gcp.json
{
  "name": "my-agent-gcp",
  "version": "1.0.0",
  "type": "module",
  "main": "index.mjs",
  "dependencies": {
    "@google-cloud/firestore": "^7.0.0"
  }
}

Non-bundleable dependencies

@google-cloud/firestore uses native gRPC bindings and must be listed as an external in esbuild and as a dependency in the deployment package.json. If you do not use Firestore-backed stores, you can omit it entirely.

Including Static Assets

If your agent uses prompt templates (YAML files) or fixture data, copy them into the output directory:

# Add to your build script
cp -R prompts dist-gcp/prompts
cp -R fixtures dist-gcp/fixtures

Deploying

Using gcloud CLI

gcloud functions deploy my-agent \
  --gen2 \
  --runtime nodejs20 \
  --region europe-west1 \
  --source dist-gcp \
  --entry-point api \
  --trigger-http \
  --allow-unauthenticated \
  --memory 512Mi \
  --timeout 300s \
  --set-env-vars "GEMINI_MODEL=gemini-3-flash-preview,GCP_PROJECT_ID=my-project" \
  --set-secrets "GOOGLE_AI_STUDIO_KEY=my-api-key-secret:latest"

Use Secret Manager for API keys

Never pass GOOGLE_AI_STUDIO_KEY as a plain environment variable. Use --set-secrets to mount it from Google Cloud Secret Manager at runtime.

Using Terraform

main.tf
resource "google_cloudfunctions2_function" "agent" {
  name     = "my-agent"
  location = "europe-west1"

  build_config {
    runtime     = "nodejs20"
    entry_point = "api"
    source {
      storage_source {
        bucket = google_storage_bucket.source.name
        object = google_storage_bucket_object.source_zip.name
      }
    }
  }

  service_config {
    max_instance_count = 10
    min_instance_count = 0
    available_memory   = "512Mi"
    timeout_seconds    = 300

    environment_variables = {
      GEMINI_MODEL   = "gemini-3-flash-preview"
      GCP_PROJECT_ID = var.project_id
    }

    secret_environment_variables {
      key        = "GOOGLE_AI_STUDIO_KEY"
      project_id = var.project_id
      secret     = "my-api-key-secret"
      version    = "latest"
    }
  }
}

Required IAM Permissions

The Cloud Function's service account needs the following roles:

Role Purpose
roles/aiplatform.user Gemini API access (if using Vertex AI endpoint)
roles/datastore.user Firestore read/write (for FirestoreTraceStore, FirestoreConversationStore)
roles/secretmanager.secretAccessor Access secrets mounted via --set-secrets
roles/storage.objectViewer Read from GCS buckets (if using GCS-backed canonical grounding)
roles/logging.logWriter Write Cloud Logging entries (granted by default)

Gemini via AI Studio vs. Vertex AI

If you use a GOOGLE_AI_STUDIO_KEY (API key from AI Studio), the aiplatform.user role is not required. The API key authenticates directly. The Vertex AI role is only needed when authenticating via service account credentials with the Vertex AI endpoint.


Environment Variables

Set these environment variables on your Cloud Function. See the full reference for all available variables.

# Required
GOOGLE_AI_STUDIO_KEY=<secret>           # Gemini API key (use Secret Manager)

# Recommended
GEMINI_MODEL=gemini-3-flash-preview     # Model name
GCP_PROJECT_ID=my-project               # Enables Firestore-backed stores

# Optional
DEBUG=*                                 # Enable debug logging
GROUNDING_MODE=chunks_only              # Grounding policy
KB_STORE=fileSearchStores/my-store      # Default knowledge base store
TRACE_CAPTURE_CONTENT=true              # Capture full prompt/response in traces

Troubleshooting

Cold starts are slow

Set --min-instances 1 to keep at least one instance warm. This eliminates cold starts but incurs idle costs. For 2nd gen functions, cold starts are typically 1--3 seconds.

Function times out

The default timeout is 60 seconds. Agent executions with RAG retrieval and multiple tool calls can take longer. Increase with --timeout 300s (maximum 540 seconds for 2nd gen).

esbuild errors with @google-cloud/firestore

Always mark it as --external:@google-cloud/firestore. This package uses native Node.js bindings that esbuild cannot bundle. Include it in your deployment package.json instead.

CORS errors in the browser

Ensure you pass cors: { origin: "*" } (or your specific origin) to the handler factory. The gcpHttp adapter does not add CORS headers automatically -- the handler factory does.