# Atlas – open-source deep research you own

> Source: <https://github.com/steel-dev/atlas>
> Published: 2026-06-25 16:30:47+00:00

**Research Agent for the Open Web**

``` js
import { Atlas } from "@steel-dev/atlas";
import { anthropic } from "@ai-sdk/anthropic";

const atlas = new Atlas({ model: anthropic("claude-fable-5") });
const { report } = await atlas.research(
  "What's changing in browser automation for AI agents?",
);
```

One-off query, no install:

```
ANTHROPIC_API_KEY=sk-ant-... npx @steel-dev/atlas "How do reasoning models differ from standard LLMs?"
npm install @steel-dev/atlas ai @ai-sdk/anthropic
# or @ai-sdk/openai / @ai-sdk/google
```

**Search:** `tavily.search()`

/ `exa.search()`

/ `brave.search()`

(auto from env keys), or `native.search({ model })`

to use the model provider's own web search.

**Fetch:** `basic.fetch()`

by default; `steel.fetch()`

for JS-rendered pages when `STEEL_API_KEY`

is set.

``` js
import { Atlas, exa, steel, basic } from "@steel-dev/atlas";

const atlas = new Atlas({
  model,
  search: exa.search(),
  fetch: [basic.fetch(), steel.fetch({ proxy: true })],
});
```

Register other research agents — including Atlas itself — as a fleet. Atlas decomposes the question, routes each sub-task to the best-fit researcher (`query → report`

), runs them in isolation, then synthesizes one report. With no `researchers`

set it stays a single spine run.

``` js
import { Atlas, exa, perplexity, parallel } from "@steel-dev/atlas";

const atlas = new Atlas({
  model,
  researchers: {
    exa: exa.agent(), // Exa deep-research (via exa-js)
    perplexity: perplexity.agent(), // Perplexity Sonar
    parallel: parallel.agent(), // parallel.ai task API
  },
});

await atlas.research("Compare X across academic and shopping angles");
```

Any `query → report`

worker plugs in via `researcher({ description, research })`

— `description`

drives routing. `atlas.asResearcher(description)`

exposes an Atlas instance as a worker, so fan-out can recurse. A worker returns `{ report, sources }`

and may add `cost`

(USD).

Plug in domain sources with `researchTool`

: anything via `ctx.addSource`

flows through the same source store:

``` js
import { Atlas, researchTool } from "@steel-dev/atlas";

const atlas = new Atlas({
  model,
  tools: {
    pubmed_search: researchTool({
      description: "Search PubMed.",
      inputSchema: z.object({ query: z.string() }),
      execute: async ({ query }, ctx) => {
        const studies = await pubmed.search(query, { signal: ctx.signal });
        for (const s of studies)
          ctx.addSource({ url: s.url, title: s.title, content: s.abstract });
        return studies.map((s) => `- ${s.title}`).join("\n");
      },
    }),
  },
});
```

`ctx`

(`ToolContext`

) provides `addSource({ url, title?, content })`

, `fetchText(url)`

(a guarded fetch returning extracted text or `null`

), `log(message)`

, and `signal`

.

**Models per role:** the top-level `model`

is the lead; override the stage models with `models.research`

and `models.write`

. Each defaults to the lead model when unset.

**Z.ai / GLM:** use Z.ai through its OpenAI-compatible endpoint:

``` js
import { createOpenAI } from "@ai-sdk/openai";
import { Atlas, tavily } from "@steel-dev/atlas";

const zai = createOpenAI({
  apiKey: process.env.ZAI_API_KEY!,
  baseURL: "https://api.z.ai/api/paas/v4",
});

const atlas = new Atlas({
  model: zai.chat("glm-5.2"),
  search: tavily.search(),
});
```

The examples also accept `--provider zai`

with `ZAI_API_KEY`

/ `ATLAS_ZAI_API_KEY`

. Configure `tavily.search()`

, `exa.search()`

, or `brave.search()`

for search; Atlas does not map Z.ai's web-search API to an AI SDK native search tool.

**Concurrency:** `concurrency: { models: 4, io: 10 }`

or `ATLAS_MODEL_CONCURRENCY`

/ `ATLAS_IO_CONCURRENCY`

.

``` js
const run = atlas.start(question, { effort: "balanced" });

let preview = "";
for await (const e of run.events()) {
  if (e.type === "report.reset") preview = "";
  if (e.type === "report.delta") preview += e.text;
  if (e.type === "source.fetched") console.error(e.url);
}

const result = await run.result();
```

`report.delta`

carries the working draft as it is written and revised; `report.reset`

precedes each rewrite, so clear your preview buffer on it. `report.completed`

(and `result.report`

) is canonical after citation binding. `run.finish()`

synthesizes from whatever's gathered so far; `run.abort()`

discards it. Late subscribers get full event history.

Journaled runs replay completed model/search/fetch calls without new provider charges after a crash or deploy:

``` js
import { Atlas, fileStore } from "@steel-dev/atlas";

const store = fileStore("./runs");
const atlas = new Atlas({ model, store });
atlas.start(question, { runId: "run_42" });
// …restart…
await new Atlas({ model, store }).resume("run_42");
```

Replay issues no new provider calls, but it re-charges the run's original budget and token caps as it restores progress — so a resumed run continues within the headroom left when it stopped, and `resume()`

keeps those original caps (it doesn't take a higher budget). `result.stats.costUSD`

reports only the new spend, excluding replayed calls.

```
result.report; // cited markdown
result.note; // short note on how the research was approached
result.citations; // { sourceId, marker } per cited source — marker is its [N] in the report (same contract on single-spine and orchestrated runs)
result.unboundCitations; // source ids cited in the draft that didn't resolve to a fetched source
result.warnings; // non-fatal issues (e.g. a researcher that returned nothing)
result.sources; // sources backing the report (via = fetch method, or the researcher that returned it on a fleet run)
result.stats; // cost, tokens, duration, …
result.trace; // timing/cost trace + bottleneck digest when trace !== "off"
```

Pass a `schema`

to get a typed object extracted from the finished report, returned alongside the full result. It runs as a final pass over the report, so it works on any path — single spine run, orchestrated fleet, or an outsourced researcher — and its cost is charged to the same budget and counted in `result.stats.costUSD`

.

``` js
import { z } from "zod";

const r = await atlas.research("Acme's latest annual revenue and CEO?", {
  schema: z.object({ revenue: z.string(), ceo: z.string() }),
});

r.object; // typed: { revenue: string; ceo: string }
r.report; // the cited report it was extracted from
```

One meter for everything. Pick an effort, or override any cap with `budget`

.

| effort | ~budget | sources | tokens |
|---|---|---|---|
`fast` |
$0.50 | 15 | 200K |
`balanced` |
$2.50 | 40 | 1M |
`deep` |
$10 | 100 | 4M |
`max` |
$40 | 250 | 16M |

```
await atlas.research(question, {
  effort: "deep",
  budget: { maxUSD: 5, maxTokens: 50_000_000 },
});
```

`maxUSD`

is a **best-effort target, not a hard ceiling.** The meter checks between agent turns, so a run stops *starting* work once spent, but in-flight calls (up to `concurrency.models`

) can overshoot. Pricing comes from a built-in table that can lag provider changes — pass `pricing`

to correct a rate when the cap must be accurate.

The real backstops are **price-independent** — each defaults to the effort row above and is enforced regardless of prices:

`budget.maxTokens`

— input + output tokens run-wide (cache reads excluded). Checked between turns like`maxUSD`

, but never drifts with prices.`budget.maxSources`

,`budget.maxDurationMs`

— fetched-source and wall-clock caps.

`result.stats`

reports `budgetExhausted`

and `tokensExhausted`

so you can see which limit bound the run. Leave headroom on `maxUSD`

, or set a provider-side spend limit, when the cap is truly hard.

`result.stats.stopReason`

folds those into one value — `"completed"`

, `"finished"`

(`run.finish()`

), or a binding cap (`"budget"`

, `"tokens"`

, `"timeout"`

). When several apply, the most proximate wins.

```
git clone https://github.com/steel-dev/atlas.git && cd atlas
npm install && npm run dev -- "your question"
```

`examples/cli.ts`

: terminal runs`examples/serve.ts`

: SSE web app`evals/`

: BrowseComp + DRACO (`npm run eval:browsecomp`

,`npm run eval:draco`

)

MIT
