Gemini 3.6 Flash landed on July 21, 2026 with three capabilities that matter for developers building agents: a configurable thinking budget that exposes the model's internal reasoning, genuine parallel tool execution in a single turn, and a 17% reduction in output tokens on complex multi-step workflows compared to 3.5 Flash. The pricing drop to $1.50 per million input tokens and $7.50 per million output tokens makes it the most cost-effective thinking model in Google's lineup.
This tutorial walks through every layer — basic completion, thinking mode configuration, streaming thought tokens, parallel function calling, and a complete market research agent built as a Next.js 15 SSE route.
Prerequisites
Before starting, make sure you have:
- Node.js 20+ and pnpm installed
- A Google AI Studio account with an API key (free tier works for this tutorial)
- Familiarity with TypeScript async/await patterns
- Basic Next.js 15 App Router knowledge
What You'll Build
A "Market Intel" research agent that:
- Accepts a natural-language query about a company or market
- Uses Gemini 3.6 Flash's thinking mode to plan its research approach
- Executes three tool calls in parallel — stock quotes, recent news, and competitor data
- Streams both the model's reasoning and the final analysis to a React client
- Exposes the full pipeline as a Next.js 15 API route with Server-Sent Events
By the end you will have a production-ready pattern for any agent that needs to reason about what to research before gathering data.
Step 1: Project Setup
Scaffold a Next.js 15 app and install the Google Generative AI SDK:
pnpm create next-app@latest market-intel --typescript --tailwind --app --no-src-dir
cd market-intel
pnpm add @google/genai zodCreate a .env.local file in the project root:
GEMINI_API_KEY=your_key_from_ai_studioGet your key from Google AI Studio. The free tier includes 15 requests per minute and 1,500 requests per day, more than enough for development.
Step 2: Configure the Gemini Client
Create a shared client module so every file uses the same instance and model constant:
// lib/gemini.ts
import { GoogleGenAI } from "@google/genai";
if (!process.env.GEMINI_API_KEY) {
throw new Error("GEMINI_API_KEY is not set in .env.local");
}
export const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
export const MODEL = "gemini-3.6-flash";Verify the setup with a quick script before building anything complex:
// scripts/test-connection.ts
import { ai, MODEL } from "../lib/gemini";
const response = await ai.models.generateContent({
model: MODEL,
contents: "Reply with: Gemini 3.6 Flash is online.",
});
console.log(response.text);Run it with:
npx tsx scripts/test-connection.tsYou should see the confirmation message within one to two seconds.
Step 3: Enable Thinking Mode
Gemini 3.6 Flash supports a thinkingBudget configuration that maps to the three tiers you see in Google Antigravity's UI. The budget is measured in thinking tokens — the internal reasoning tokens the model uses before generating its response.
| Budget Value | Antigravity Tier | Best Use Case |
|---|---|---|
| 512–2048 | Low | CRUD generation, classification, simple Q&A |
| 4096–8192 | Medium | Feature implementation, research queries, analysis |
| -1 (unlimited) | High | Architecture decisions, complex multi-step reasoning |
| 0 | Disabled | Maximum speed, no visible reasoning |
Create a helper that maps tier names to the correct budget value:
// lib/thinking.ts
export type ThinkingTier = "low" | "medium" | "high" | "disabled";
export function thinkingConfig(tier: ThinkingTier) {
const budgets: Record<ThinkingTier, number> = {
low: 1024,
medium: 8192,
high: -1,
disabled: 0,
};
return { thinkingBudget: budgets[tier] };
}Tip: Start every new agent with
"medium"tier. Run the same task ten times with"medium"and"high"and compare output quality. In most practical cases, medium is indistinguishable from high at roughly half the thinking token cost.
Step 4: Stream Thinking Tokens
Thinking models emit two categories of content in the stream: thought parts (the internal reasoning, colored amber in Antigravity's UI) and text parts (the visible response). Your application can surface both.
// lib/stream-agent.ts
import { ai, MODEL } from "./gemini";
import { thinkingConfig } from "./thinking";
export type StreamChunk =
| { type: "thinking"; text: string }
| { type: "response"; text: string };
export async function* streamWithThinking(
prompt: string,
tier: "low" | "medium" | "high" = "medium"
): AsyncGenerator<StreamChunk> {
const stream = await ai.models.generateContentStream({
model: MODEL,
contents: prompt,
config: {
thinkingConfig: thinkingConfig(tier),
},
});
for await (const chunk of stream) {
const parts = chunk.candidates?.[0]?.content?.parts ?? [];
for (const part of parts) {
if (part.thought && part.text) {
yield { type: "thinking", text: part.text };
} else if (part.text) {
yield { type: "response", text: part.text };
}
}
}
}Test the stream in a small script:
// scripts/test-thinking.ts
import { streamWithThinking } from "../lib/stream-agent";
console.log("--- THINKING ---");
for await (const chunk of streamWithThinking(
"Compare PostgreSQL and SQLite for a mobile-first application. Consider offline sync, bundle size, and query complexity.",
"medium"
)) {
if (chunk.type === "thinking") process.stdout.write("[T] " + chunk.text);
else process.stdout.write("[R] " + chunk.text);
}The [T] lines show the model working through trade-offs before it commits to the final answer — the same reasoning that would otherwise be invisible.
Step 5: Define Tool Declarations
Gemini 3.6 Flash uses the same function declaration format as earlier Gemini models. Define each tool as a FunctionDeclaration with a JSON Schema parameters object:
// lib/tools.ts
import type { FunctionDeclaration } from "@google/genai";
export const researchTools: FunctionDeclaration[] = [
{
name: "get_stock_quote",
description:
"Get the current stock price, market cap, and key financial ratios for a ticker symbol.",
parameters: {
type: "object",
properties: {
symbol: {
type: "string",
description: "Stock ticker e.g. NVDA, MSFT, GOOGL",
},
},
required: ["symbol"],
},
},
{
name: "search_news",
description: "Search recent news articles for a company or topic.",
parameters: {
type: "object",
properties: {
query: { type: "string", description: "News search query" },
days: {
type: "integer",
description: "Number of past days to search. Default is 7.",
},
},
required: ["query"],
},
},
{
name: "get_competitors",
description:
"Return a list of direct competitors and approximate market share for a company.",
parameters: {
type: "object",
properties: {
company: {
type: "string",
description: "Company name or ticker symbol",
},
},
required: ["company"],
},
},
];Step 6: Implement Tool Handlers
For this tutorial the handlers return mock data. In production you would replace each function with a real API call — Alpha Vantage for stock quotes, Tavily or Serper for news, and a curated database for competitor data.
// lib/tool-handlers.ts
export async function executeToolCall(
name: string,
args: Record<string, unknown>
): Promise<unknown> {
switch (name) {
case "get_stock_quote":
return getStockQuote(args.symbol as string);
case "search_news":
return searchNews(args.query as string, (args.days as number) ?? 7);
case "get_competitors":
return getCompetitors(args.company as string);
default:
throw new Error(`Unknown tool: ${name}`);
}
}
function getStockQuote(symbol: string) {
const data: Record<string, object> = {
NVDA: { price: 1247.5, change: "+3.2%", marketCap: "3.1T", peRatio: 48.2 },
MSFT: { price: 512.3, change: "+0.8%", marketCap: "3.8T", peRatio: 35.1 },
GOOGL: { price: 198.45, change: "+1.5%", marketCap: "2.4T", peRatio: 28.7 },
};
return data[symbol.toUpperCase()] ?? { error: `Symbol ${symbol} not found` };
}
function searchNews(query: string, days: number) {
return {
articles: [
{
title: `${query}: Key Developments`,
summary:
"AI infrastructure spending continues to accelerate as hyperscalers expand data center capacity...",
source: "Reuters",
publishedAt: new Date().toISOString(),
},
{
title: `${query} — Analyst Upgrades`,
summary:
"Three major investment banks raised their price targets following strong quarterly results...",
source: "Bloomberg",
publishedAt: new Date(Date.now() - 86400000).toISOString(),
},
],
totalResults: 2,
daysSearched: days,
};
}
function getCompetitors(company: string) {
return {
company,
competitors: [
{ name: "AMD", marketShare: "18%", focus: "GPU, CPU" },
{ name: "Intel", marketShare: "12%", focus: "CPU, GPU" },
{ name: "Qualcomm", marketShare: "8%", focus: "Edge AI, Mobile" },
],
updatedAt: new Date().toISOString(),
};
}Step 7: Build the Parallel Agent Loop
The critical insight when working with Gemini 3.6 Flash tool calls: when the model returns multiple functionCall parts in a single response turn, it is explicitly requesting parallel execution. Running them sequentially adds unnecessary latency — use Promise.all to fire all tool calls concurrently, then send all results back in a single user turn.
// lib/agent-loop.ts
import type { Content, FunctionCall, Part } from "@google/genai";
import { ai, MODEL } from "./gemini";
import { thinkingConfig } from "./thinking";
import { researchTools } from "./tools";
import { executeToolCall } from "./tool-handlers";
export interface AgentResult {
thoughts: string[];
answer: string;
toolCalls: Array<{ name: string; args: Record<string, unknown> }>;
}
export async function runResearchAgent(
query: string
): Promise<AgentResult> {
const history: Content[] = [
{ role: "user", parts: [{ text: query }] },
];
const thoughts: string[] = [];
const toolCalls: AgentResult["toolCalls"] = [];
for (let turn = 0; turn < 6; turn++) {
const response = await ai.models.generateContent({
model: MODEL,
contents: history,
config: {
tools: [{ functionDeclarations: researchTools }],
thinkingConfig: thinkingConfig("medium"),
},
});
const parts = response.candidates?.[0]?.content?.parts ?? [];
// Collect thinking tokens
for (const part of parts) {
if (part.thought && part.text) {
thoughts.push(part.text);
}
}
// Find function calls in this turn
const fnCalls = parts.filter(
(p): p is Part & { functionCall: FunctionCall } => !!p.functionCall
);
if (fnCalls.length === 0) {
// No more tool calls — return the text answer
const answer = parts
.filter((p) => !p.thought && p.text)
.map((p) => p.text ?? "")
.join("");
return { thoughts, answer, toolCalls };
}
// Append model turn
history.push({ role: "model", parts });
// Track tool calls for the result
for (const { functionCall } of fnCalls) {
toolCalls.push({
name: functionCall.name,
args: (functionCall.args ?? {}) as Record<string, unknown>,
});
}
// Execute all tool calls in parallel
const toolResults = await Promise.all(
fnCalls.map(async ({ functionCall }) => {
const result = await executeToolCall(
functionCall.name,
(functionCall.args ?? {}) as Record<string, unknown>
);
return {
functionResponse: {
name: functionCall.name,
response: { output: result },
},
};
})
);
// Send all results back in a single user turn
history.push({ role: "user", parts: toolResults });
}
return {
thoughts,
answer: "Research completed — maximum iterations reached.",
toolCalls,
};
}Step 8: Next.js 15 API Route with SSE Streaming
Expose the agent as a streaming Server-Sent Events endpoint. The client will receive real-time updates for thinking tokens, tool execution, and the final answer:
// app/api/research/route.ts
import { NextRequest } from "next/server";
import type { Content, FunctionCall, Part } from "@google/genai";
import { ai, MODEL } from "@/lib/gemini";
import { thinkingConfig } from "@/lib/thinking";
import { researchTools } from "@/lib/tools";
import { executeToolCall } from "@/lib/tool-handlers";
export const maxDuration = 120;
export async function POST(req: NextRequest) {
const { query } = await req.json() as { query: string };
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
const send = (data: object) =>
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(data)}\n\n`)
);
const history: Content[] = [
{ role: "user", parts: [{ text: query }] },
];
for (let turn = 0; turn < 6; turn++) {
const response = await ai.models.generateContent({
model: MODEL,
contents: history,
config: {
tools: [{ functionDeclarations: researchTools }],
thinkingConfig: thinkingConfig("medium"),
},
});
const parts = response.candidates?.[0]?.content?.parts ?? [];
// Stream thinking tokens as they appear
for (const part of parts) {
if (part.thought && part.text) {
send({ type: "thinking", text: part.text });
}
}
const fnCalls = parts.filter(
(p): p is Part & { functionCall: FunctionCall } => !!p.functionCall
);
if (fnCalls.length === 0) {
const answer = parts
.filter((p) => !p.thought && p.text)
.map((p) => p.text ?? "")
.join("");
send({ type: "result", text: answer });
break;
}
history.push({ role: "model", parts });
send({ type: "tools_start", names: fnCalls.map((f) => f.functionCall.name) });
const toolResults = await Promise.all(
fnCalls.map(async ({ functionCall }) => {
const result = await executeToolCall(
functionCall.name,
(functionCall.args ?? {}) as Record<string, unknown>
);
send({ type: "tool_done", name: functionCall.name });
return {
functionResponse: {
name: functionCall.name,
response: { output: result },
},
};
})
);
history.push({ role: "user", parts: toolResults });
}
controller.close();
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}Step 9: React Client Component
Build a simple dashboard that renders the SSE stream in real time:
// app/page.tsx
"use client";
import { useState } from "react";
type Event =
| { type: "thinking"; text: string }
| { type: "tools_start"; names: string[] }
| { type: "tool_done"; name: string }
| { type: "result"; text: string };
export default function ResearchPage() {
const [query, setQuery] = useState("");
const [events, setEvents] = useState<Event[]>([]);
const [loading, setLoading] = useState(false);
async function runResearch() {
setEvents([]);
setLoading(true);
const res = await fetch("/api/research", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query }),
});
const reader = res.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
for (const line of decoder.decode(value).split("\n")) {
if (!line.startsWith("data: ")) continue;
try {
const event = JSON.parse(line.slice(6)) as Event;
setEvents((prev) => [...prev, event]);
} catch {
// skip incomplete chunks
}
}
}
setLoading(false);
}
return (
<main className="max-w-3xl mx-auto p-8 space-y-6">
<h1 className="text-2xl font-bold">Market Intel Agent</h1>
<p className="text-gray-600 text-sm">
Powered by Gemini 3.6 Flash with thinking mode and parallel tool execution.
</p>
<div className="flex gap-2">
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && !loading && query && runResearch()}
placeholder="e.g. NVIDIA competitive landscape in AI chips"
className="flex-1 border rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
onClick={runResearch}
disabled={loading || !query.trim()}
className="bg-blue-600 text-white px-5 py-2 rounded-lg disabled:opacity-50 hover:bg-blue-700 transition-colors"
>
{loading ? "Researching…" : "Research"}
</button>
</div>
<div className="space-y-2">
{events.map((event, i) => (
<div
key={i}
className={`rounded-lg p-3 text-sm ${
event.type === "thinking"
? "bg-amber-50 border border-amber-200 text-amber-900 font-mono text-xs"
: event.type === "tools_start"
? "bg-blue-50 border border-blue-200 text-blue-800"
: event.type === "tool_done"
? "bg-emerald-50 border border-emerald-200 text-emerald-800"
: "bg-white border shadow-sm whitespace-pre-wrap"
}`}
>
{event.type === "thinking" && (
<span>
<span className="font-semibold">Thinking: </span>
{event.text}
</span>
)}
{event.type === "tools_start" && (
<span>
<span className="font-semibold">Calling in parallel: </span>
{event.names.join(", ")}
</span>
)}
{event.type === "tool_done" && (
<span>
<span className="font-semibold">Done: </span>
{event.name}
</span>
)}
{event.type === "result" && (
<div>
<p className="font-semibold mb-2">Analysis:</p>
{event.text}
</div>
)}
</div>
))}
</div>
</main>
);
}Cost Optimization Patterns
Gemini 3.6 Flash is already the most affordable thinking model in its class, but there are several patterns that reduce costs further:
Right-size the thinking budget. For queries where the answer is known in advance to be straightforward, set thinkingBudget to 512 or even 0. Thinking tokens count as output tokens at $7.50/M.
Cache the system prompt. If you add a system instruction with company context, background data, or tool usage guidelines, that text repeats every turn. Place it in the first message with a cache hint to pay for it once.
Short-circuit with a memo. In agent loops where the same tool might be called with identical arguments across turns, maintain a Map keyed by name + JSON.stringify(args) and return the cached result instead of making a second tool call.
Batch related queries. The 1M-token context window lets you send ten company queries in a single call with a structured JSON prompt and get ten responses back, rather than making ten separate API requests.
Troubleshooting
401 API_KEY_INVALID — Verify the key is from Google AI Studio, not from a Vertex AI service account. The two auth systems are separate.
thinkingConfig has no effect — Confirm the model is gemini-3.6-flash. The Flash-Lite variant (gemini-3.6-flash-lite) does not support thinking mode and ignores the thinkingBudget field.
Thought parts are empty — Thought tokens require thinkingBudget to be greater than 0. Setting thinkingBudget: 0 disables thinking entirely for maximum speed. Use at least 512 to see reasoning output.
Agent loop never terminates — Add a hard turn cap as shown above. If the loop still runs long, log history at each iteration and look for a tool call that keeps returning an error — the model may be retrying a broken handler indefinitely.
CORS errors in the browser — Never call the Gemini API directly from client-side JavaScript; it exposes your API key. Always route through a Next.js API route or server action, as shown in Step 8.
Next Steps
- Replace mock tool handlers with Alpha Vantage for real stock data and Tavily for grounded news search
- Swap the manual SSE loop for
useChatfrom Vercel AI SDK 7 with agoogleprovider adapter - Explore Google's built-in
code_executiontool for inline Python data analysis within the agent loop - Try the Gemini Live API for real-time voice-driven research queries
- Route Gemini through your own gateway for cost visibility and rate limiting: LiteLLM Proxy tutorial
- Compare reasoning output with an earlier-generation model using the Google Gemini API TypeScript tutorial
Conclusion
Gemini 3.6 Flash brings thinking-mode reasoning, parallel tool execution, and a one-million-token context window to a price point that makes multi-step agents economically viable for production workloads. The 17% token reduction per complex workflow is a genuine cost lever, not a benchmark artifact — it translates into fewer agent turns, lower bills, and faster time to the final answer. With the patterns in this tutorial — thinkingBudget configuration, the parallel Promise.all tool loop, and streaming thought tokens to a React client — you have the foundation to build agents that reason explicitly about what to gather before they start gathering it.