@steel-dev/atlas 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +219 -0
- package/dist/agent.d.ts +34 -0
- package/dist/agent.js +133 -0
- package/dist/async.d.ts +19 -0
- package/dist/async.js +172 -0
- package/dist/atlas.d.ts +19 -0
- package/dist/atlas.js +69 -0
- package/dist/budget.d.ts +64 -0
- package/dist/budget.js +336 -0
- package/dist/checklist.d.ts +115 -0
- package/dist/checklist.js +297 -0
- package/dist/cli.js +38700 -0
- package/dist/config.d.ts +80 -0
- package/dist/config.js +109 -0
- package/dist/context.d.ts +26 -0
- package/dist/context.js +250 -0
- package/dist/custom-tools.d.ts +26 -0
- package/dist/custom-tools.js +33 -0
- package/dist/defaults.d.ts +10 -0
- package/dist/defaults.js +37 -0
- package/dist/economy.d.ts +12 -0
- package/dist/economy.js +6 -0
- package/dist/env.d.ts +1 -0
- package/dist/env.js +8 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +11 -0
- package/dist/event-hub.d.ts +11 -0
- package/dist/event-hub.js +83 -0
- package/dist/events.d.ts +105 -0
- package/dist/events.js +1 -0
- package/dist/html-extract.d.ts +21 -0
- package/dist/html-extract.js +459 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +26 -0
- package/dist/memory.d.ts +2 -0
- package/dist/memory.js +38 -0
- package/dist/model.d.ts +49 -0
- package/dist/model.js +630 -0
- package/dist/orchestrate.d.ts +5 -0
- package/dist/orchestrate.js +277 -0
- package/dist/pdf-extract.d.ts +5 -0
- package/dist/pdf-extract.js +20 -0
- package/dist/prompts.d.ts +2 -0
- package/dist/prompts.js +6 -0
- package/dist/providers/domain/arxiv.d.ts +6 -0
- package/dist/providers/domain/arxiv.js +83 -0
- package/dist/providers/domain/clinicaltrials.d.ts +6 -0
- package/dist/providers/domain/clinicaltrials.js +104 -0
- package/dist/providers/domain/edgar.d.ts +10 -0
- package/dist/providers/domain/edgar.js +92 -0
- package/dist/providers/domain/index.d.ts +14 -0
- package/dist/providers/domain/index.js +7 -0
- package/dist/providers/domain/openalex.d.ts +7 -0
- package/dist/providers/domain/openalex.js +128 -0
- package/dist/providers/domain/pubmed.d.ts +8 -0
- package/dist/providers/domain/pubmed.js +123 -0
- package/dist/providers/domain/semantic-scholar.d.ts +6 -0
- package/dist/providers/domain/semantic-scholar.js +112 -0
- package/dist/providers/domain/shared.d.ts +12 -0
- package/dist/providers/domain/shared.js +39 -0
- package/dist/providers/domain/wikipedia.d.ts +6 -0
- package/dist/providers/domain/wikipedia.js +71 -0
- package/dist/providers/exa-agent.d.ts +9 -0
- package/dist/providers/exa-agent.js +67 -0
- package/dist/providers/fetch.d.ts +66 -0
- package/dist/providers/fetch.js +675 -0
- package/dist/providers/parallel-agent.d.ts +11 -0
- package/dist/providers/parallel-agent.js +100 -0
- package/dist/providers/perplexity-agent.d.ts +17 -0
- package/dist/providers/perplexity-agent.js +86 -0
- package/dist/providers/search.d.ts +65 -0
- package/dist/providers/search.js +433 -0
- package/dist/providers/store.d.ts +48 -0
- package/dist/providers/store.js +217 -0
- package/dist/researcher.d.ts +20 -0
- package/dist/researcher.js +3 -0
- package/dist/robots.d.ts +16 -0
- package/dist/robots.js +146 -0
- package/dist/roles.d.ts +6 -0
- package/dist/roles.js +4 -0
- package/dist/run.d.ts +65 -0
- package/dist/run.js +371 -0
- package/dist/safe-dispatcher.d.ts +16 -0
- package/dist/safe-dispatcher.js +32 -0
- package/dist/safety.d.ts +23 -0
- package/dist/safety.js +206 -0
- package/dist/sandbox.d.ts +22 -0
- package/dist/sandbox.js +228 -0
- package/dist/search-normalize.d.ts +2 -0
- package/dist/search-normalize.js +13 -0
- package/dist/source-documents.d.ts +77 -0
- package/dist/source-documents.js +421 -0
- package/dist/sources.d.ts +57 -0
- package/dist/sources.js +1 -0
- package/dist/spine.d.ts +19 -0
- package/dist/spine.js +722 -0
- package/dist/state.d.ts +90 -0
- package/dist/state.js +27 -0
- package/dist/structured.d.ts +7 -0
- package/dist/structured.js +18 -0
- package/dist/tools.d.ts +33 -0
- package/dist/tools.js +1187 -0
- package/dist/trace-digest.d.ts +11 -0
- package/dist/trace-digest.js +309 -0
- package/dist/trace.d.ts +225 -0
- package/dist/trace.js +278 -0
- package/dist/trail.d.ts +15 -0
- package/dist/trail.js +74 -0
- package/dist/url.d.ts +1 -0
- package/dist/url.js +25 -0
- package/package.json +107 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Steel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USAGE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# Atlas
|
|
4
|
+
|
|
5
|
+
**Research Agent for the Open Web**
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { Atlas } from "@steel-dev/atlas";
|
|
9
|
+
import { anthropic } from "@ai-sdk/anthropic";
|
|
10
|
+
|
|
11
|
+
const atlas = new Atlas({ model: anthropic("claude-fable-5") });
|
|
12
|
+
const { report } = await atlas.research(
|
|
13
|
+
"What's changing in browser automation for AI agents?",
|
|
14
|
+
);
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @steel-dev/atlas ai @ai-sdk/anthropic
|
|
21
|
+
# or @ai-sdk/openai / @ai-sdk/google
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Search:** `tavily.search()` / `exa.search()` / `brave.search()` (auto from env keys), or `native.search({ model })` to use the model provider's own web search.
|
|
25
|
+
|
|
26
|
+
**Fetch:** `basic.fetch()` by default; `steel.fetch()` for JS-rendered pages when `STEEL_API_KEY` is set.
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { Atlas, exa, steel, basic } from "@steel-dev/atlas";
|
|
30
|
+
|
|
31
|
+
const atlas = new Atlas({
|
|
32
|
+
model,
|
|
33
|
+
search: exa.search(),
|
|
34
|
+
fetch: [basic.fetch(), steel.fetch({ proxy: true })],
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Orchestrate researchers
|
|
39
|
+
|
|
40
|
+
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.
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { Atlas, exa, perplexity, parallel } from "@steel-dev/atlas";
|
|
44
|
+
|
|
45
|
+
const atlas = new Atlas({
|
|
46
|
+
model,
|
|
47
|
+
researchers: {
|
|
48
|
+
exa: exa.agent(), // Exa deep-research (via exa-js)
|
|
49
|
+
perplexity: perplexity.agent(), // Perplexity Sonar
|
|
50
|
+
parallel: parallel.agent(), // parallel.ai task API
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await atlas.research("Compare X across academic and shopping angles");
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
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).
|
|
58
|
+
|
|
59
|
+
## Extend it
|
|
60
|
+
|
|
61
|
+
Plug in domain sources with `researchTool`: anything via `ctx.addSource` flows through the same source store:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { Atlas, researchTool } from "@steel-dev/atlas";
|
|
65
|
+
|
|
66
|
+
const atlas = new Atlas({
|
|
67
|
+
model,
|
|
68
|
+
tools: {
|
|
69
|
+
pubmed_search: researchTool({
|
|
70
|
+
description: "Search PubMed.",
|
|
71
|
+
inputSchema: z.object({ query: z.string() }),
|
|
72
|
+
execute: async ({ query }, ctx) => {
|
|
73
|
+
const studies = await pubmed.search(query, { signal: ctx.signal });
|
|
74
|
+
for (const s of studies)
|
|
75
|
+
ctx.addSource({ url: s.url, title: s.title, content: s.abstract });
|
|
76
|
+
return studies.map((s) => `- ${s.title}`).join("\n");
|
|
77
|
+
},
|
|
78
|
+
}),
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`ctx` (`ToolContext`) provides `addSource({ url, title?, content })`, `fetchText(url)` (a guarded fetch returning extracted text or `null`), `log(message)`, and `signal`.
|
|
84
|
+
|
|
85
|
+
**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.
|
|
86
|
+
|
|
87
|
+
**Z.ai / GLM:** use Z.ai through its OpenAI-compatible endpoint:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
91
|
+
import { Atlas, tavily } from "@steel-dev/atlas";
|
|
92
|
+
|
|
93
|
+
const zai = createOpenAI({
|
|
94
|
+
apiKey: process.env.ZAI_API_KEY!,
|
|
95
|
+
baseURL: "https://api.z.ai/api/paas/v4",
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const atlas = new Atlas({
|
|
99
|
+
model: zai.chat("glm-5.2"),
|
|
100
|
+
search: tavily.search(),
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
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.
|
|
105
|
+
|
|
106
|
+
**Concurrency:** `concurrency: { models: 4, io: 10 }` or `ATLAS_MODEL_CONCURRENCY` / `ATLAS_IO_CONCURRENCY`.
|
|
107
|
+
|
|
108
|
+
## Stream it
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
const run = atlas.start(question, { effort: "balanced" });
|
|
112
|
+
|
|
113
|
+
let preview = "";
|
|
114
|
+
for await (const e of run.events()) {
|
|
115
|
+
if (e.type === "report.reset") preview = "";
|
|
116
|
+
if (e.type === "report.delta") preview += e.text;
|
|
117
|
+
if (e.type === "source.fetched") console.error(e.url);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const result = await run.result();
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`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.
|
|
124
|
+
|
|
125
|
+
## Resume & providers
|
|
126
|
+
|
|
127
|
+
Journaled runs replay completed model/search/fetch calls without new provider charges after a crash or deploy:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
import { Atlas, fileStore } from "@steel-dev/atlas";
|
|
131
|
+
|
|
132
|
+
const store = fileStore("./runs");
|
|
133
|
+
const atlas = new Atlas({ model, store });
|
|
134
|
+
atlas.start(question, { runId: "run_42" });
|
|
135
|
+
// …restart…
|
|
136
|
+
await new Atlas({ model, store }).resume("run_42");
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
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.
|
|
140
|
+
|
|
141
|
+
## Results
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
result.report; // cited markdown
|
|
145
|
+
result.note; // short note on how the research was approached
|
|
146
|
+
result.citations; // { sourceId, marker } per cited source — marker is its [N] in the report (same contract on single-spine and orchestrated runs)
|
|
147
|
+
result.unboundCitations; // source ids cited in the draft that didn't resolve to a fetched source
|
|
148
|
+
result.warnings; // non-fatal issues (e.g. a researcher that returned nothing)
|
|
149
|
+
result.sources; // sources backing the report (via = fetch method, or the researcher that returned it on a fleet run)
|
|
150
|
+
result.stats; // cost, tokens, duration, …
|
|
151
|
+
result.trace; // timing/cost trace + bottleneck digest when trace !== "off"
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Structured output
|
|
155
|
+
|
|
156
|
+
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`.
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
import { z } from "zod";
|
|
160
|
+
|
|
161
|
+
const r = await atlas.research("Acme's latest annual revenue and CEO?", {
|
|
162
|
+
schema: z.object({ revenue: z.string(), ceo: z.string() }),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
r.object; // typed: { revenue: string; ceo: string }
|
|
166
|
+
r.report; // the cited report it was extracted from
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Budget
|
|
170
|
+
|
|
171
|
+
One meter for everything. Pick an effort, or override any cap with `budget`.
|
|
172
|
+
|
|
173
|
+
| effort | ~budget | sources | tokens |
|
|
174
|
+
| ---------- | ------- | ------- | ------ |
|
|
175
|
+
| `fast` | $0.50 | 15 | 200K |
|
|
176
|
+
| `balanced` | $2.50 | 40 | 1M |
|
|
177
|
+
| `deep` | $10 | 100 | 4M |
|
|
178
|
+
| `max` | $40 | 250 | 16M |
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
await atlas.research(question, {
|
|
182
|
+
effort: "deep",
|
|
183
|
+
budget: { maxUSD: 5, maxTokens: 50_000_000 },
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`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.
|
|
188
|
+
|
|
189
|
+
The real backstops are **price-independent** — each defaults to the effort row above and is enforced regardless of prices:
|
|
190
|
+
|
|
191
|
+
- `budget.maxTokens` — input + output tokens run-wide (cache reads excluded). Checked between turns like `maxUSD`, but never drifts with prices.
|
|
192
|
+
- `budget.maxSources`, `budget.maxDurationMs` — fetched-source and wall-clock caps.
|
|
193
|
+
|
|
194
|
+
`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.
|
|
195
|
+
|
|
196
|
+
`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.
|
|
197
|
+
|
|
198
|
+
## Safety
|
|
199
|
+
|
|
200
|
+
Untrusted web content is quarantined (data, not instructions). Fetches pass SSRF guards hop-by-hop; `run_code` runs in a memory-capped V8 isolate with no network, filesystem, or host access. Direct fetch honors robots.txt.
|
|
201
|
+
|
|
202
|
+
The isolate needs the optional `isolated-vm` dependency; without it, `run_code` is dropped from the toolset and the run proceeds without it — Atlas never falls back to an unsandboxed evaluator.
|
|
203
|
+
|
|
204
|
+
The SSRF guard validates DNS at check time but can't pin the connection, so an attacker controlling DNS can defeat it via rebinding. Treat it as defense-in-depth — for hostile targets, run behind network-level egress controls that block private ranges.
|
|
205
|
+
|
|
206
|
+
## Dev
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
git clone https://github.com/steel-dev/atlas.git && cd atlas
|
|
210
|
+
npm install && npm run dev -- "your question"
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
- `examples/cli.ts`: terminal runs
|
|
214
|
+
- `examples/serve.ts`: SSE web app
|
|
215
|
+
- `evals/`: BrowseComp + DRACO (`npm run eval:browsecomp`, `npm run eval:draco`)
|
|
216
|
+
|
|
217
|
+
## License
|
|
218
|
+
|
|
219
|
+
MIT
|
package/dist/agent.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type ModelMessage } from "ai";
|
|
2
|
+
import type { BudgetGrant } from "./budget.js";
|
|
3
|
+
import type { AgentRole } from "./events.js";
|
|
4
|
+
import { type ModelRole } from "./model.js";
|
|
5
|
+
import { type RunCtx } from "./state.js";
|
|
6
|
+
import { type ToolName } from "./tools.js";
|
|
7
|
+
export declare const CONTEXT_BUDGET_STOP = "context budget reached";
|
|
8
|
+
export interface AgentSpec {
|
|
9
|
+
role: AgentRole;
|
|
10
|
+
modelRole: ModelRole;
|
|
11
|
+
task: string;
|
|
12
|
+
system: string;
|
|
13
|
+
tools: ToolName[];
|
|
14
|
+
grant: BudgetGrant;
|
|
15
|
+
depth: number;
|
|
16
|
+
parentId?: string | undefined;
|
|
17
|
+
maxTurns?: number | undefined;
|
|
18
|
+
maxOutputTokensPerStep?: number | undefined;
|
|
19
|
+
maxContextTokens?: number | undefined;
|
|
20
|
+
tokenCeiling?: number | undefined;
|
|
21
|
+
captureMessages?: boolean | undefined;
|
|
22
|
+
memoryCursor?: number | undefined;
|
|
23
|
+
forceFirstTool?: ToolName | undefined;
|
|
24
|
+
stopWhenSatisfied?: (() => boolean) | undefined;
|
|
25
|
+
}
|
|
26
|
+
export interface AgentResult {
|
|
27
|
+
agentId: string;
|
|
28
|
+
note: string;
|
|
29
|
+
spentUSD: number;
|
|
30
|
+
stopReason: string;
|
|
31
|
+
messages?: ModelMessage[];
|
|
32
|
+
}
|
|
33
|
+
export declare function runAgent(rctx: RunCtx, spec: AgentSpec): Promise<AgentResult>;
|
|
34
|
+
export declare function describeBudget(rctx: RunCtx): string;
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { generateText, stepCountIs } from "ai";
|
|
2
|
+
import { stubToolResultWindow } from "./memory.js";
|
|
3
|
+
import { MODEL_CALL_MAX_RETRIES, totalFreshTokens, } from "./model.js";
|
|
4
|
+
import { budgetStatusLine } from "./state.js";
|
|
5
|
+
import { buildAgentTools } from "./tools.js";
|
|
6
|
+
import { currentFrame, withTraceFrame } from "./trace.js";
|
|
7
|
+
const TASK_PREVIEW_CHARS = 300;
|
|
8
|
+
export const CONTEXT_BUDGET_STOP = "context budget reached";
|
|
9
|
+
export async function runAgent(rctx, spec) {
|
|
10
|
+
const agentId = `agent_${rctx.agentSequence.next++}`;
|
|
11
|
+
const actx = {
|
|
12
|
+
agentId,
|
|
13
|
+
role: spec.role,
|
|
14
|
+
grant: spec.grant,
|
|
15
|
+
depth: spec.depth,
|
|
16
|
+
};
|
|
17
|
+
const toolNames = spec.tools;
|
|
18
|
+
const tools = buildAgentTools(rctx, actx, toolNames);
|
|
19
|
+
const model = rctx.bindModel(spec.modelRole, spec.grant);
|
|
20
|
+
let governorReason = null;
|
|
21
|
+
let lastText = "";
|
|
22
|
+
const recorder = rctx.recorder;
|
|
23
|
+
const parentSpanId = recorder ? currentFrame()?.parentSpanId : undefined;
|
|
24
|
+
const agentSpanId = recorder?.mintSpanId();
|
|
25
|
+
const agentStartedAt = recorder ? recorder.now() : 0;
|
|
26
|
+
const logicalAgentId = agentId;
|
|
27
|
+
const agentFrame = {
|
|
28
|
+
agentId,
|
|
29
|
+
logicalAgentId,
|
|
30
|
+
role: spec.role,
|
|
31
|
+
depth: spec.depth,
|
|
32
|
+
site: spec.role,
|
|
33
|
+
...(agentSpanId ? { parentSpanId: agentSpanId } : {}),
|
|
34
|
+
};
|
|
35
|
+
const result = await withTraceFrame(recorder, agentFrame, () => generateText({
|
|
36
|
+
model,
|
|
37
|
+
system: spec.system,
|
|
38
|
+
prompt: spec.task,
|
|
39
|
+
tools,
|
|
40
|
+
maxRetries: MODEL_CALL_MAX_RETRIES,
|
|
41
|
+
abortSignal: rctx.signal,
|
|
42
|
+
...(spec.maxOutputTokensPerStep
|
|
43
|
+
? { maxOutputTokens: spec.maxOutputTokensPerStep }
|
|
44
|
+
: {}),
|
|
45
|
+
stopWhen: [
|
|
46
|
+
stepCountIs(spec.maxTurns ?? rctx.config.envelope.maxTurns),
|
|
47
|
+
({ steps }) => {
|
|
48
|
+
if (spec.grant.floored()) {
|
|
49
|
+
governorReason = "budget exhausted";
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
if (spec.tokenCeiling !== undefined &&
|
|
53
|
+
totalFreshTokens(rctx.usage) >= spec.tokenCeiling) {
|
|
54
|
+
governorReason = "token ceiling reached";
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
const runReason = rctx.stopReason();
|
|
58
|
+
if (runReason) {
|
|
59
|
+
governorReason = runReason;
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
if (spec.stopWhenSatisfied?.()) {
|
|
63
|
+
governorReason = "ledger satisfied";
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
if (spec.maxContextTokens !== undefined) {
|
|
67
|
+
const inputTokens = steps.at(-1)?.usage.inputTokens;
|
|
68
|
+
if (typeof inputTokens === "number" &&
|
|
69
|
+
inputTokens >= spec.maxContextTokens) {
|
|
70
|
+
governorReason = CONTEXT_BUDGET_STOP;
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
prepareStep: ({ stepNumber, messages }) => {
|
|
78
|
+
const memory = spec.memoryCursor !== undefined
|
|
79
|
+
? {
|
|
80
|
+
messages: stubToolResultWindow(messages, spec.memoryCursor),
|
|
81
|
+
}
|
|
82
|
+
: {};
|
|
83
|
+
if (spec.forceFirstTool && stepNumber === 0) {
|
|
84
|
+
return {
|
|
85
|
+
toolChoice: {
|
|
86
|
+
type: "tool",
|
|
87
|
+
toolName: spec.forceFirstTool,
|
|
88
|
+
},
|
|
89
|
+
...memory,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return memory;
|
|
93
|
+
},
|
|
94
|
+
onStepFinish: (step) => {
|
|
95
|
+
if (step.text?.trim())
|
|
96
|
+
lastText = step.text.trim();
|
|
97
|
+
},
|
|
98
|
+
}));
|
|
99
|
+
const note = result.text.trim() || lastText;
|
|
100
|
+
const stopReason = governorReason ??
|
|
101
|
+
(result.finishReason === "stop" || result.finishReason === "tool-calls"
|
|
102
|
+
? "completed"
|
|
103
|
+
: result.finishReason);
|
|
104
|
+
if (recorder && agentSpanId) {
|
|
105
|
+
recorder.recordAgentSpan({
|
|
106
|
+
id: agentSpanId,
|
|
107
|
+
...(parentSpanId ? { parentId: parentSpanId } : {}),
|
|
108
|
+
site: spec.role,
|
|
109
|
+
agentId,
|
|
110
|
+
logicalAgentId,
|
|
111
|
+
role: spec.role,
|
|
112
|
+
t0: agentStartedAt,
|
|
113
|
+
t1: recorder.now(),
|
|
114
|
+
costUSD: spec.grant.spentUSD(),
|
|
115
|
+
status: rctx.signal?.aborted ? "aborted" : "ok",
|
|
116
|
+
attrs: {
|
|
117
|
+
depth: spec.depth,
|
|
118
|
+
task: spec.task.slice(0, TASK_PREVIEW_CHARS),
|
|
119
|
+
stopReason,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
agentId,
|
|
125
|
+
note,
|
|
126
|
+
spentUSD: spec.grant.spentUSD(),
|
|
127
|
+
stopReason,
|
|
128
|
+
...(spec.captureMessages ? { messages: result.response.messages } : {}),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
export function describeBudget(rctx) {
|
|
132
|
+
return budgetStatusLine(rctx);
|
|
133
|
+
}
|
package/dist/async.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
|
|
2
|
+
export declare function withTimeout<T>(ms: number, parentSignal: AbortSignal | undefined, label: string, fn: (signal: AbortSignal) => Promise<T>): Promise<T>;
|
|
3
|
+
export interface ConcurrencyGate {
|
|
4
|
+
run<T>(fn: () => Promise<T>): Promise<T>;
|
|
5
|
+
acquire(): Promise<() => void>;
|
|
6
|
+
}
|
|
7
|
+
export declare function createConcurrencyGate(limit: number): ConcurrencyGate;
|
|
8
|
+
export declare function createDynamicConcurrencyGate(limitFn: () => number): ConcurrencyGate;
|
|
9
|
+
export interface AdaptiveLimit {
|
|
10
|
+
value(): number;
|
|
11
|
+
onSuccess(): void;
|
|
12
|
+
onThrottle(): void;
|
|
13
|
+
}
|
|
14
|
+
export declare function createAdaptiveLimit(opts: {
|
|
15
|
+
start: number;
|
|
16
|
+
min: number;
|
|
17
|
+
max: number;
|
|
18
|
+
}): AdaptiveLimit;
|
|
19
|
+
export declare function mapWithConcurrency<T, R>(items: T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
|
package/dist/async.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
export async function sleep(ms, signal) {
|
|
2
|
+
if (ms <= 0) {
|
|
3
|
+
signal?.throwIfAborted();
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
if (signal?.aborted)
|
|
7
|
+
throw signal.reason ?? new Error("Aborted");
|
|
8
|
+
await new Promise((resolve, reject) => {
|
|
9
|
+
const timer = setTimeout(() => {
|
|
10
|
+
signal?.removeEventListener("abort", onAbort);
|
|
11
|
+
resolve();
|
|
12
|
+
}, ms);
|
|
13
|
+
const onAbort = () => {
|
|
14
|
+
clearTimeout(timer);
|
|
15
|
+
reject(signal?.reason ?? new Error("Aborted"));
|
|
16
|
+
};
|
|
17
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export async function withTimeout(ms, parentSignal, label, fn) {
|
|
21
|
+
const timeout = AbortSignal.timeout(ms);
|
|
22
|
+
const combined = parentSignal
|
|
23
|
+
? AbortSignal.any([parentSignal, timeout])
|
|
24
|
+
: timeout;
|
|
25
|
+
return await Promise.race([
|
|
26
|
+
fn(combined),
|
|
27
|
+
new Promise((_, reject) => {
|
|
28
|
+
const onAbort = () => reject(timeout.aborted && !parentSignal?.aborted
|
|
29
|
+
? new Error(`${label} timed out after ${Math.round(ms / 1000)}s`)
|
|
30
|
+
: (combined.reason ?? new Error("Aborted")));
|
|
31
|
+
if (combined.aborted)
|
|
32
|
+
onAbort();
|
|
33
|
+
else
|
|
34
|
+
combined.addEventListener("abort", onAbort, { once: true });
|
|
35
|
+
}),
|
|
36
|
+
]);
|
|
37
|
+
}
|
|
38
|
+
class Semaphore {
|
|
39
|
+
limit;
|
|
40
|
+
active = 0;
|
|
41
|
+
waiting = [];
|
|
42
|
+
constructor(limit) {
|
|
43
|
+
this.limit = limit;
|
|
44
|
+
}
|
|
45
|
+
async run(fn) {
|
|
46
|
+
const release = await this.acquire();
|
|
47
|
+
try {
|
|
48
|
+
return await fn();
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
release();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async acquire() {
|
|
55
|
+
await this.acquireSlot();
|
|
56
|
+
let released = false;
|
|
57
|
+
return () => {
|
|
58
|
+
if (released)
|
|
59
|
+
return;
|
|
60
|
+
released = true;
|
|
61
|
+
this.releaseSlot();
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async acquireSlot() {
|
|
65
|
+
if (this.active < this.limit) {
|
|
66
|
+
this.active++;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
await new Promise((resolve) => this.waiting.push(() => {
|
|
70
|
+
this.active++;
|
|
71
|
+
resolve();
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
releaseSlot() {
|
|
75
|
+
this.active--;
|
|
76
|
+
this.waiting.shift()?.();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export function createConcurrencyGate(limit) {
|
|
80
|
+
const normalized = Number.isFinite(limit)
|
|
81
|
+
? Math.max(1, Math.floor(limit))
|
|
82
|
+
: 1;
|
|
83
|
+
return new Semaphore(normalized);
|
|
84
|
+
}
|
|
85
|
+
class DynamicSemaphore {
|
|
86
|
+
limitFn;
|
|
87
|
+
active = 0;
|
|
88
|
+
waiting = [];
|
|
89
|
+
constructor(limitFn) {
|
|
90
|
+
this.limitFn = limitFn;
|
|
91
|
+
}
|
|
92
|
+
async run(fn) {
|
|
93
|
+
const release = await this.acquire();
|
|
94
|
+
try {
|
|
95
|
+
return await fn();
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
release();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async acquire() {
|
|
102
|
+
await this.acquireSlot();
|
|
103
|
+
let released = false;
|
|
104
|
+
return () => {
|
|
105
|
+
if (released)
|
|
106
|
+
return;
|
|
107
|
+
released = true;
|
|
108
|
+
this.releaseSlot();
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
limit() {
|
|
112
|
+
const raw = this.limitFn();
|
|
113
|
+
return Number.isFinite(raw) ? Math.max(1, Math.floor(raw)) : 1;
|
|
114
|
+
}
|
|
115
|
+
async acquireSlot() {
|
|
116
|
+
if (this.active < this.limit()) {
|
|
117
|
+
this.active++;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
await new Promise((resolve) => this.waiting.push(() => {
|
|
121
|
+
this.active++;
|
|
122
|
+
resolve();
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
releaseSlot() {
|
|
126
|
+
this.active--;
|
|
127
|
+
while (this.active < this.limit() && this.waiting.length > 0) {
|
|
128
|
+
this.waiting.shift()?.();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
export function createDynamicConcurrencyGate(limitFn) {
|
|
133
|
+
return new DynamicSemaphore(limitFn);
|
|
134
|
+
}
|
|
135
|
+
export function createAdaptiveLimit(opts) {
|
|
136
|
+
const min = Math.max(1, Math.floor(opts.min));
|
|
137
|
+
const max = Math.max(min, Math.floor(opts.max));
|
|
138
|
+
const clamp = (n) => Math.min(max, Math.max(min, n));
|
|
139
|
+
let width = clamp(Math.floor(opts.start));
|
|
140
|
+
let cleanRun = 0;
|
|
141
|
+
return {
|
|
142
|
+
value: () => width,
|
|
143
|
+
onSuccess: () => {
|
|
144
|
+
if (width >= max)
|
|
145
|
+
return;
|
|
146
|
+
cleanRun++;
|
|
147
|
+
if (cleanRun >= width * 2) {
|
|
148
|
+
width = clamp(width + 1);
|
|
149
|
+
cleanRun = 0;
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
onThrottle: () => {
|
|
153
|
+
width = clamp(Math.floor(width / 2));
|
|
154
|
+
cleanRun = 0;
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
export async function mapWithConcurrency(items, limit, fn) {
|
|
159
|
+
const results = new Array(items.length);
|
|
160
|
+
const normalizedLimit = Number.isFinite(limit)
|
|
161
|
+
? Math.max(1, Math.floor(limit))
|
|
162
|
+
: 1;
|
|
163
|
+
let next = 0;
|
|
164
|
+
const workers = Array.from({ length: Math.min(normalizedLimit, items.length) }, async () => {
|
|
165
|
+
while (next < items.length) {
|
|
166
|
+
const index = next++;
|
|
167
|
+
results[index] = await fn(items[index], index);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
await Promise.all(workers);
|
|
171
|
+
return results;
|
|
172
|
+
}
|
package/dist/atlas.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { FlexibleSchema } from "ai";
|
|
2
|
+
import type { AtlasConfig, ResearchOptions } from "./config.js";
|
|
3
|
+
import { type ResearchResult, type ResearchRun, type ResumeOptions } from "./run.js";
|
|
4
|
+
export type { AtlasConfig, ResearchOptions } from "./config.js";
|
|
5
|
+
import type { Researcher } from "./researcher.js";
|
|
6
|
+
import type { StructuredResult } from "./structured.js";
|
|
7
|
+
export declare class Atlas {
|
|
8
|
+
#private;
|
|
9
|
+
constructor(config: AtlasConfig);
|
|
10
|
+
research(question: string, options?: ResearchOptions): Promise<ResearchResult>;
|
|
11
|
+
research<T>(question: string, options: ResearchOptions & {
|
|
12
|
+
schema: FlexibleSchema<T>;
|
|
13
|
+
}): Promise<StructuredResult<T>>;
|
|
14
|
+
start(question: string, options?: ResearchOptions): ResearchRun;
|
|
15
|
+
asResearcher(description: string): Researcher;
|
|
16
|
+
resume(runId: string, options?: ResumeOptions): Promise<ResearchRun>;
|
|
17
|
+
close(): Promise<void>;
|
|
18
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
19
|
+
}
|
package/dist/atlas.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { resumeRun, startRun, } from "./run.js";
|
|
2
|
+
import { AtlasError } from "./errors.js";
|
|
3
|
+
export class Atlas {
|
|
4
|
+
#config;
|
|
5
|
+
#closed = false;
|
|
6
|
+
#runs = new Set();
|
|
7
|
+
constructor(config) {
|
|
8
|
+
this.#config = config;
|
|
9
|
+
}
|
|
10
|
+
async research(question, options = {}) {
|
|
11
|
+
if (this.#closed)
|
|
12
|
+
throw new AtlasError("Atlas is closed; create a new instance", "config");
|
|
13
|
+
const { schema, ...rest } = options;
|
|
14
|
+
const result = await this.#startRun(question, rest, schema).result();
|
|
15
|
+
return schema ? result : result;
|
|
16
|
+
}
|
|
17
|
+
start(question, options = {}) {
|
|
18
|
+
if (this.#closed)
|
|
19
|
+
throw new AtlasError("Atlas is closed; create a new instance", "config");
|
|
20
|
+
return this.#startRun(question, options);
|
|
21
|
+
}
|
|
22
|
+
#startRun(question, options, schema) {
|
|
23
|
+
const run = startRun({
|
|
24
|
+
config: this.#config,
|
|
25
|
+
question,
|
|
26
|
+
options,
|
|
27
|
+
...(schema ? { schema } : {}),
|
|
28
|
+
});
|
|
29
|
+
this.#track(run);
|
|
30
|
+
return run;
|
|
31
|
+
}
|
|
32
|
+
asResearcher(description) {
|
|
33
|
+
return {
|
|
34
|
+
description,
|
|
35
|
+
research: async (query, ctx) => {
|
|
36
|
+
const result = await this.#startRun(query, {
|
|
37
|
+
budget: { maxUSD: ctx.budget.maxUSD },
|
|
38
|
+
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
39
|
+
}).result();
|
|
40
|
+
return {
|
|
41
|
+
report: result.report,
|
|
42
|
+
sources: result.sources.map((s) => ({ url: s.url, title: s.title })),
|
|
43
|
+
cost: result.stats.costUSD,
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
async resume(runId, options) {
|
|
49
|
+
if (this.#closed)
|
|
50
|
+
throw new AtlasError("Atlas is closed; create a new instance", "config");
|
|
51
|
+
const run = await resumeRun(runId, this.#config, options);
|
|
52
|
+
this.#track(run);
|
|
53
|
+
return run;
|
|
54
|
+
}
|
|
55
|
+
async close() {
|
|
56
|
+
this.#closed = true;
|
|
57
|
+
await Promise.allSettled([...this.#runs].map((run) => run.abort()));
|
|
58
|
+
}
|
|
59
|
+
[Symbol.asyncDispose]() {
|
|
60
|
+
return this.close();
|
|
61
|
+
}
|
|
62
|
+
#track(run) {
|
|
63
|
+
this.#runs.add(run);
|
|
64
|
+
void run
|
|
65
|
+
.result()
|
|
66
|
+
.then(() => undefined, () => undefined)
|
|
67
|
+
.finally(() => this.#runs.delete(run));
|
|
68
|
+
}
|
|
69
|
+
}
|