Create AI Agents Using Just JavaScript (Research Agent Project)

A JavaScript research agent earns the name only when it can gather sources instead of feeding invented search snippets to a model. You will build a Node.js command-line agent that calls Gemini’s Google Search tool, returns a report, and prints the citations supplied by the API.

What this JavaScript research agent does

The program accepts a topic, asks Gemini 3.6 Flash to research it, and gives the model access to Google Search. The response includes the synthesized report plus URL citations attached to the model output.

  1. Read the topic from the command line.
  2. Send an interaction with the Google Search tool enabled.
  3. Collect URL citations from model-output annotations.
  4. Print the report and a deduplicated source list.

If you need the concepts before the implementation, read what AI agents are, then compare this tool-using project with a basic JavaScript AI wrapper.

The model can choose Google Search during the task, but the program does not keep durable memory, edit files, or run an open-ended planning loop.

Set up Node.js and the Gemini SDK

The @google/genai package requires Node.js 20 or newer. On July 22, 2026, I installed Node.js 26.5.0 and @google/genai 2.13.0 in a clean workspace for this project.

npm install --no-save --package-lock=false @google/genai

Create an API key in Google AI Studio, then keep it in an environment variable. The Gemini API key guide walks through the account setup.

export GEMINI_API_KEY="your-key-here"

Keep the key out of browser JavaScript because Google’s API-key guidance directs production web and mobile apps to a backend that visitors cannot inspect for the credential.

Build the research agent

Create a file named research-agent.mjs and add the code below. The separation between research() and main() lets you test the search request and citation formatter without sending a paid request.

import { GoogleGenAI } from "@google/genai";
import { pathToFileURL } from "node:url";

export function collectCitations(interaction) {
  const citations = new Map();

  for (const step of interaction.steps ?? []) {
    if (step.type !== "model_output") continue;

    for (const block of step.content ?? []) {
      if (block.type !== "text") continue;

      for (const annotation of block.annotations ?? []) {
        if (annotation.type === "url_citation" && annotation.url) {
          citations.set(annotation.url, annotation.title || annotation.url);
        }
      }
    }
  }

  return [...citations].map(([url, title]) => ({ title, url }));
}

export async function research(client, topic) {
  const interaction = await client.interactions.create({
    model: "gemini-3.6-flash",
    input: `Research this topic and write a concise report with key findings, disagreements between sources, and practical next steps: ${topic}`,
    tools: [{ type: "google_search" }],
  });

  return {
    report: interaction.output_text,
    citations: collectCitations(interaction),
  };
}

async function main() {
  const topic = process.argv.slice(2).join(" ").trim();

  if (!process.env.GEMINI_API_KEY) {
    console.error("Set GEMINI_API_KEY before running the research agent.");
    process.exitCode = 1;
    return;
  }

  if (!topic) {
    console.error('Usage: node research-agent.mjs "your research topic"');
    process.exitCode = 1;
    return;
  }

  const client = new GoogleGenAI({});
  const result = await research(client, topic);

  console.log(result.report);
  console.log("\nSources");

  for (const citation of result.citations) {
    console.log(`- ${citation.title}: ${citation.url}`);
  }
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  main().catch((error) => {
    console.error(error instanceof Error ? error.message : String(error));
    process.exitCode = 1;
  });
}

Run the agent

Pass the research topic after the file name. Quoting it keeps the full phrase in one command-line argument.

./node_modules/.bin/node research-agent.mjs "How JavaScript runtimes handle asynchronous I/O"

A successful request prints the report first, followed by a Sources heading and one line per cited URL. The exact report and source list change with the topic and the pages returned by Google Search.

I exercised the SDK surface, search-request construction, report return value, citation extraction, missing-key branch, and missing-topic branch with Node.js 26.5.0. The server had no Gemini credential, so I did not reproduce model-generated report text as if it came from a live request.

If the environment variable is absent, the tested command exits with this message.

Set GEMINI_API_KEY before running the research agent.

How the search and citation path works

The GoogleGenAI client reads GEMINI_API_KEY from the environment, while interactions.create() sends the topic, model name, and google_search tool declaration in one request.

Gemini can issue a search call before producing its model output. collectCitations() then walks the text-block annotations and deduplicates URLs with a Map.

Even a cited report is model-generated text, so open important sources before using it for medical, legal, financial, security, or publication decisions.

Where the agent boundary sits

Tool use alone does not make every model call autonomous. This program delegates one bounded research task and exposes the evidence returned by the API.

CapabilityIncluded here
Search the webYes, through Gemini’s Google Search tool
Return citationsYes, from URL annotations
Repeat searches until a quality target is metNo
Durable memory between runsNo
Take external actionsNo
Human approval before high-impact workYou provide it outside the script

That boundary is useful for a beginner project because you can inspect the complete control flow. Add more autonomy only when you can test each tool, cap the number of steps, and decide which actions require approval.

Useful extensions

The next change should improve a measurable part of the task rather than add another framework. These extensions preserve the small control surface.

  • Save the report and citation list as JSON after validating the output path.
  • Add a second pass that checks whether every major claim has at least one citation.
  • Accept a list of trusted domains and reject citations outside it.
  • Add a custom function tool for an action you can test locally.
  • Create a fixture suite for empty results, duplicate citations, API errors, and malformed annotations.

Frequently asked questions

These answers clarify the security and architecture choices in the project.

Aditya Gupta
Aditya Gupta
Articles: 529