Gemini Interactions and Deep Research
GeminiInteractions is a thin, Gemini-specific layer over the
Interactions API of the
@google/genai SDK. It differs from LLM in two ways:
- State lives on the server: each call returns an
idand the next one continues the conversation withpreviousInteractionId, without resending the history. - It is the path to Google's managed agents, such as Deep Research, which runs in the background for several minutes.
The Interactions API and the agents are in preview: agent names and fields change often.
npm install jangada-ai @google/genai@latestRequires @google/genai 1.33 or newer (the version that ships
client.interactions). With an older SDK, jangada throws UnsupportedError
asking you to upgrade. The key comes from GEMINI_API_KEY or GOOGLE_API_KEY,
or from apiKey in the constructor.
import { GeminiInteractions, webSearch } from "jangada-ai";
const gi = new GeminiInteractions({ model: "gemini-3.8-flash" });
const r = await gi.create("Who won the 2002 World Cup?", { tools: [webSearch()] });
console.log(r.text);
for (const c of r.citations) console.log("-", c.title, c.url);
// continues the conversation on the server, without resending history
const r2 = await gi.create("And the top scorer?", { previousInteractionId: r.id ?? undefined });When to use GeminiInteractions and when to use LLM
new LLM("gemini", ...): the default path. Switch providers without changing code, with retry, fallback, cache and guardrails. Gemini's native tools work there too.GeminiInteractions: when you want server-side state, the managed agents or long background tasks with polling. It has no retry or fallback.
API
const gi = new GeminiInteractions({
apiKey: process.env.GEMINI_API_KEY, // default: GEMINI_API_KEY or GOOGLE_API_KEY
model: "gemini-3.8-flash", // default for create(); optional when the call uses agent
vertexai: false, // true uses Vertex AI
clientOptions: {}, // passed to the GoogleGenAI constructor
});| Method | What it does |
|---|---|
create(input, options) | Creates an interaction or starts an agent |
stream(input, options) | Same as create, yielding InteractionEvent (async iterator) |
resumeStream(id, { lastEventId }) | Resumes an interrupted stream |
get(id), cancel(id), delete(id) | State, cancellation and removal |
wait(target, { pollIntervalMs, timeoutMs, onUpdate }) | Polls until it leaves queued/in_progress |
run(input, { tools, maxIterations, onToolCall, onToolResult }) | Function calling loop with local execution |
deepResearch(prompt, { agent, wait, pollIntervalMs, timeoutMs, onUpdate }) | Starts Deep Research in the background |
close() | Closes the native client |
create/stream options: model or agent (one of them), agentConfig,
tools, system, previousInteractionId, store, background,
responseFormat, toolChoice, maxTokens, seed, stop, thinkingLevel,
thinkingSummaries, extra (body fields in snake_case) and signal.
InteractionResult
id,status(queued,in_progress,requires_action,completed,failed,cancelled,incomplete,budget_exceeded),done,requiresAction.text,steps(as returned by the API, in snake_case),citations({ type, url, title, start, end }).functionCalls: pending calls (InteractionFunctionCallwithid,name,argsand.result(output, { isError })).usage,cost,parsed,model,agent,errors,raw.- Filled in by
run:toolTrace,iterations,stoppedByLimit,usageTotal,costTotal,costComplete. raiseForStatus(): throwsProviderErrorif it ended infailed,cancelledorbudget_exceeded.
Tools
tools accepts:
- jangada function tools (
tool({ name, parameters, execute })), which become{ type: "function" }; - canonical native tools:
webSearch(),urlContext(),codeExecution(),fileSearch(...),googleMaps(...)andnativeTool("gemini", spec); - raw API objects (
{ type: "google_search" },{ type: "mcp_server", ... }).
Native tools that do not exist in the Interactions API (such as
imageGeneration()) or a nativeTool for another provider throw
UnsupportedError. Unlike Python, a plain function does not become a tool: use
tool(...).
Function calling with local execution (run)
run creates the interaction, executes the functions the model asks for and
replies with previousInteractionId until the final answer or maxIterations
(default 10). A tool that fails, has no execute or was vetoed (onToolCall
returning false) becomes an error result, and the model can react.
import * as z from "zod";
import { tool } from "jangada-ai";
const rate = tool({
name: "exchange_rate",
description: "Exchange rate of the currency in Brazilian reais.",
parameters: z.object({ currency: z.string() }),
execute: ({ currency }) => ({ USD: 5.4, EUR: 5.9 })[currency.toUpperCase()] ?? 0,
});
const r3 = await gi.run("How much are 100 dollars in reais?", { tools: [rate] });
console.log(r3.text, r3.toolTrace.map((t) => t.name), r3.costTotal);To drive the loop by hand, use create and answer functionCalls:
let r4 = await gi.create("How much are 100 dollars?", { tools: [rate] });
if (r4.requiresAction) {
const replies = [];
for (const c of r4.functionCalls) {
replies.push(c.result(await rate.execute?.(await rate.parseArgs(c.args))));
}
r4 = await gi.create(replies, { previousInteractionId: r4.id ?? undefined, tools: [rate] });
}Structured output
responseFormat accepts Zod 4.2+, Standard Schema or jsonSchema(...). The
validated result goes in parsed; JSON outside the schema throws
OutputValidationError. A raw JSON Schema object is sent to the API without
validation and without parsed.
const Summary = z.object({ title: z.string(), points: z.array(z.string()) });
const r5 = await gi.create<z.infer<typeof Summary>>("Summarize the history of frevo.", {
responseFormat: Summary,
});
console.log(r5.parsed?.title);Streaming
stream yields InteractionEvent with type in created, status,
step_start, text, thought, delta, step_stop, completed (with the
final result) and error (throws ProviderError).
for await (const ev of gi.stream("Explain RAG in one sentence.")) {
if (ev.type === "text") process.stdout.write(ev.text ?? "");
}Deep Research (background)
const report = await gi.deepResearch("Overview of the open-source LLM market in 2026, in 5 topics.", {
onUpdate: (x) => console.log("status:", x.status),
timeoutMs: 30 * 60_000,
});
console.log(report.text);- Runs with
background: trueandstore: true(the API requiresstorein background) and polls every 10 s (pollIntervalMs). wait: falsereturns the result right away inqueued/in_progress. Later, usegi.wait(r)orgi.get(r.id).- When
timeoutMsruns out it throwsAPITimeoutError. The interaction keeps running on the server and you can resume withwaitorget. signalstops the polling and thegetin flight.- The default agent is
DEEP_RESEARCH_AGENT("deep-research-preview-04-2026"). Other agents go inagent, withagentConfigpassed through as is. - A task takes minutes and consumes from 100 thousand to millions of tokens.
Usage and cost
Usage follows the library contract: inputTokens (input plus tool result
tokens), outputTokens (includes thoughts), reasoningTokens,
cacheReadTokens, toolUseTokens, serverToolRequests and totalTokens. Cost
is computed when there is a model. With agent it stays null, since there is
no price table per agent.
Limitations
temperaturedoes not exist in the Interactions API: it is accepted and ignored.maxTokens,seed,stopandthinkingLevelgo intogeneration_config.- No retry, fallback, cache or guardrails (use
LLMfor that). streamdoes not run theruntool loop.vertexai: trueuses Vertex AI, but availability of the Interactions API there depends on the project and region.
Differences from Python
- Async only. Python's
await_iswaithere. - Times in milliseconds:
pollIntervalMsandtimeoutMsinstead ofpoll_intervalandtimeout(seconds). - Every call accepts
signal. - SDK requirement:
@google/genai1.33+ (in Python,google-genai2.3+).
Related: Providers, Tools, Structured output.
RAG
Embeddings, chunking, vector stores (in-memory, pgvector and MongoDB), hybrid search, rerank and incremental indexing in the jangada-ai/rag subpath.
A2A (Agent2Agent)
Expose a jangada-ai Agent as an A2A server: A2AHandler and buildA2aHandler in jangada-ai/a2a (web-standard, runs on edge) and serveA2a in jangada-ai/node.