{"slug": "gemini-function-calling-is-not-an-agent-runtime", "title": "Gemini Function Calling Is Not an Agent Runtime", "summary": "A developer explains that Gemini's function calling is not an agent runtime, emphasizing that the model proposes tool calls but the application must handle validation, authorization, and execution. The post outlines a production-grade pipeline with schema validation, policy checks, and idempotent execution, using the Google Gen AI SDK and Gemini 2.5 Flash as examples.", "body_md": "Gemini function calling makes tool use look simple.\n\nYou describe a function, provide its input schema, and let the model decide whether the user's request requires it. A traveler asks, \"Find hotels in Paris under $250,\" Gemini requests `search_hotels`\n\n, your application executes it, and the model turns the result into a useful answer.\n\nThat is an important capability, but it is not an agent runtime.\n\nFunction calling tells your application what the model proposes to do. It does not decide whether the action is authorized, whether the arguments are trustworthy, whether the same action already succeeded, or whether a retry would make the situation worse.\n\nThe model proposes. The runtime disposes.\n\nWith the Google Gen AI SDK, a function declaration can look like this:\n\n``` js\nimport { GoogleGenAI, Type } from \"@google/genai\";\n\nconst ai = new GoogleGenAI({\n  apiKey: process.env.GEMINI_API_KEY,\n});\n\nconst searchHotels = {\n  name: \"search_hotels\",\n  description: \"Search available hotels in a city under an optional nightly price\",\n  parameters: {\n    type: Type.OBJECT,\n    properties: {\n      city: {\n        type: Type.STRING,\n        description: \"City and country, for example Paris, France\",\n      },\n      maxNightlyPriceUsd: {\n        type: Type.NUMBER,\n        description: \"Maximum nightly price in US dollars\",\n      },\n    },\n    required: [\"city\"],\n  },\n};\n\nconst response = await ai.models.generateContent({\n  model: \"gemini-2.5-flash\",\n  contents: \"Find hotels in Paris under $250 per night\",\n  config: {\n    tools: [{ functionDeclarations: [searchHotels] }],\n  },\n});\n\nconst proposedCall = response.functionCalls?.[0];\n```\n\nThe returned function call contains a name and structured arguments. Google is explicit about the next boundary: the model does not execute your business function. Your application is responsible for executing it and returning the result.\n\nThat boundary is where production engineering begins.\n\n```\nUser request\n    │\n    ▼\nGemini proposes a function call\n    │\n    ▼\nSchema validation → authorization → policy → budget\n    │                    │\n    │                    └── block / request confirmation\n    ▼\nIdempotent tool executor with timeout\n    │\n    ▼\nNormalized result + decision record\n    │\n    ▼\nResult returned to Gemini for the final response\n```\n\nThe tool schema helps Gemini produce better arguments, but it should not replace runtime validation. Model-generated arguments cross the same trust boundary as an HTTP request.\n\n``` js\nimport { z } from \"zod\";\n\nconst SearchHotelsArgs = z.object({\n  city: z.string().trim().min(2).max(120),\n  maxNightlyPriceUsd: z.number().positive().max(10_000).optional(),\n});\n\ntype ExecutionContext = {\n  runId: string;\n  userId: string;\n  permissions: Set<string>;\n};\n\nfunction authorizeSearch(context: ExecutionContext) {\n  if (!context.permissions.has(\"hotel:read\")) {\n    throw new Error(\"User is not authorized to search hotel inventory\");\n  }\n}\n```\n\nValidation answers, \"Are these arguments structurally acceptable?\" Authorization answers, \"May this user perform this operation?\" Policy answers a different question: \"Should this operation happen in this workflow, at this moment?\"\n\nA read-only travel assistant may use `search_hotels`\n\nbut must never invoke `book_hotel`\n\n. A booking agent may require confirmation before a purchase. A notification agent may suppress a technically valid message because the user is sleeping or has already received the same alert.\n\nThese are product rules, not model responsibilities.\n\nA useful tool registry makes risk visible in code:\n\n```\ntype ToolDefinition<T> = {\n  risk: \"read\" | \"write\" | \"irreversible\";\n  timeoutMs: number;\n  parse: (value: unknown) => T;\n  execute: (args: T, context: ExecutionContext) => Promise<unknown>;\n};\n\nconst tools = {\n  search_hotels: {\n    risk: \"read\",\n    timeoutMs: 3_000,\n    parse: (value: unknown) => SearchHotelsArgs.parse(value),\n    execute: searchHotelInventory,\n  },\n  book_hotel: {\n    risk: \"irreversible\",\n    timeoutMs: 8_000,\n    parse: (value: unknown) => BookHotelArgs.parse(value),\n    execute: bookHotel,\n  },\n} satisfies Record<string, ToolDefinition<unknown>>;\n```\n\nBefore executing a write or irreversible tool, the runtime can require a recent user confirmation, an approval token, and an idempotency key. Deny by default when a tool is unknown or the workflow does not allow it.\n\nAgent loops make duplicate work surprisingly easy. A response may time out after the downstream system completed the action. The model may ask again because the result never reached its context. A worker may restart after committing the operation but before recording success.\n\nFor read operations, retrying may be harmless. For bookings, payments, notifications, and account updates, it may be disastrous.\n\n``` js\nasync function executeOnce<T>(\n  key: string,\n  operation: () => Promise<T>,\n): Promise<T> {\n  const previous = await idempotencyStore.get<T>(key);\n  if (previous) return previous;\n\n  const result = await operation();\n  await idempotencyStore.put(key, result, { ttlSeconds: 1_800 });\n  return result;\n}\n```\n\nBuild the key from stable business inputs, not the model's wording. For example: `userId + itineraryId + toolName + requestVersion`\n\n.\n\nA retry budget belongs in the runtime too. An agent should not turn a downstream outage into an unbounded loop of model calls and tool executions.\n\nAn empty array is ambiguous. Did the tool succeed and find nothing? Did it fail? Should the agent broaden the search or repeat the same call?\n\nReturn a discriminated result:\n\n```\ntype SearchResult =\n  | { status: \"ok\"; matches: Hotel[] }\n  | { status: \"no_match\"; reason: string; retrySameInput: false }\n  | { status: \"temporary_error\"; retryAfterMs: number }\n  | { status: \"blocked\"; reason: string };\n```\n\nThis is not only an API-design improvement. It prevents the model from interpreting operational uncertainty as permission to improvise.\n\nWhen you send the result back to Gemini, preserve the model's original function-call content and return the matching function-call ID. This is especially important for current thinking models, where the SDK preserves the context needed to continue the tool-calling turn correctly.\n\nYou do not need hidden chain-of-thought to debug an agent. You need an application-owned explanation of each consequential transition.\n\n```\nawait recordDecision({\n  runId: context.runId,\n  kind: \"tool_execution_allowed\",\n  tool: proposedCall.name,\n  policy: \"read_only_travel_search\",\n  reasonCode: \"USER_REQUEST_MATCHED_TOOL_SCOPE\",\n  idempotencyKey,\n});\n```\n\nPrefer bounded reason codes and safe summaries over raw prompts. They are easier to aggregate, safer to retain, and more useful in dashboards.\n\nBefore allowing a Gemini-proposed function call to affect the world, ask:\n\nGemini function calling provides a clean, structured bridge between a model and your application. That bridge is powerful precisely because your code still controls what crosses it.\n\nThe model can select the next action. A production runtime must decide whether that action is allowed to become real.", "url": "https://wpnews.pro/news/gemini-function-calling-is-not-an-agent-runtime", "canonical_source": "https://dev.to/raju_dandigam/gemini-function-calling-is-not-an-agent-runtime-4ijl", "published_at": "2026-08-31 06:26:07+00:00", "updated_at": "2026-08-31 06:51:41.602196+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "ai-agents", "developer-tools"], "entities": ["Gemini", "Google Gen AI SDK", "Gemini 2.5 Flash"], "alternates": {"html": "https://wpnews.pro/news/gemini-function-calling-is-not-an-agent-runtime", "markdown": "https://wpnews.pro/news/gemini-function-calling-is-not-an-agent-runtime.md", "text": "https://wpnews.pro/news/gemini-function-calling-is-not-an-agent-runtime.txt", "jsonld": "https://wpnews.pro/news/gemini-function-calling-is-not-an-agent-runtime.jsonld"}}