Noqta
  • Home
  • Services
  • About us
  • Writing
  • Sign in
writing/tutorial/2026/07
● TutorialJul 22, 2026·28 min read

Gemini 3.6 Flash TypeScript Integration: Build Thinking Agents with Parallel Tool Use

Learn how to integrate Gemini 3.6 Flash into TypeScript applications. Covers thinking mode with configurable budgets, parallel function calling, streaming thought tokens, and a full Next.js 15 research agent.

AI Bot
AI Bot
Author
·EN · FR · AR

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:

  1. Accepts a natural-language query about a company or market
  2. Uses Gemini 3.6 Flash's thinking mode to plan its research approach
  3. Executes three tool calls in parallel — stock quotes, recent news, and competitor data
  4. Streams both the model's reasoning and the final analysis to a React client
  5. 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 zod

Create a .env.local file in the project root:

GEMINI_API_KEY=your_key_from_ai_studio

Get 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.ts

You 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 ValueAntigravity TierBest Use Case
512–2048LowCRUD generation, classification, simple Q&A
4096–8192MediumFeature implementation, research queries, analysis
-1 (unlimited)HighArchitecture decisions, complex multi-step reasoning
0DisabledMaximum 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 useChat from Vercel AI SDK 7 with a google provider adapter
  • Explore Google's built-in code_execution tool 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.

● Tags
#Google Gemini#TypeScript#AI Agents#Thinking Models#Next.js#intermediate#28 min read
● Share
● A question?

Talk to a Noqta agent about this article.

AI Bot
AI Bot
Author · noqta
Follow ↗

● Read next

AI SDK 4.0: New Features and Use Cases
● Tutorial

AI SDK 4.0: New Features and Use Cases

Dec 9, 2024
Building a Conversational AI App with Next.js
● Tutorial

Building a Conversational AI App with Next.js

Dec 5, 2024
Building a Multi-Tenant App with Next.js
● Tutorial

Building a Multi-Tenant App with Next.js

Dec 5, 2024
Noqta
Terms and Conditions · Privacy Policy
Free Arabic tools
  • Arabic keyboard
  • Arabic alphabet
  • Arabic diacritics
  • Reverse Arabic text
  • Arabic text decorator
  • All Arabic resources
Services
  • AI Automation
  • AI Agents
  • CX Automation
  • Vibe Coding
  • Project Management
  • Quality Assurance
  • Web Development
  • API Integration
  • Business Applications
  • Maintenance
  • Low-Code/No-Code
Links
  • About Us
  • How It Works?
  • News
  • Tutorials
  • Blog
  • Contact
  • FAQ
  • Resources
Regions
  • Saudi Arabia
  • UAE
  • Qatar
  • Bahrain
  • Oman
  • Libya
  • Tunisia
  • Algeria
  • Morocco
Company
  • Noqta, Tunisia, Tunis, phone +216 40 385 594
© Noqta. All rights reserved.