@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/dist/model.js
ADDED
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { wrapLanguageModel } from "ai";
|
|
3
|
+
import { sleep } from "./async.js";
|
|
4
|
+
import { addTokenUsage, emptyTokenUsage, resolvePricing, usageCostUSD, } from "./budget.js";
|
|
5
|
+
import { ECONOMY } from "./economy.js";
|
|
6
|
+
import { AtlasError, errorMessage } from "./errors.js";
|
|
7
|
+
import { currentFrame } from "./trace.js";
|
|
8
|
+
export function createRunUsage() {
|
|
9
|
+
return { byRole: new Map(), replayedUSD: 0 };
|
|
10
|
+
}
|
|
11
|
+
function trackUsage(runUsage, role, usage) {
|
|
12
|
+
const existing = runUsage.byRole.get(role) ?? emptyTokenUsage();
|
|
13
|
+
addTokenUsage(existing, usage);
|
|
14
|
+
runUsage.byRole.set(role, existing);
|
|
15
|
+
}
|
|
16
|
+
export function totalFreshTokens(usage) {
|
|
17
|
+
let total = 0;
|
|
18
|
+
for (const roleUsage of usage.byRole.values()) {
|
|
19
|
+
total += roleUsage.input + roleUsage.output + roleUsage.cacheWrite;
|
|
20
|
+
}
|
|
21
|
+
return total;
|
|
22
|
+
}
|
|
23
|
+
export function tokenUsageFromV3(usage) {
|
|
24
|
+
const cacheRead = usage.inputTokens.cacheRead ?? 0;
|
|
25
|
+
const cacheWrite = usage.inputTokens.cacheWrite ?? 0;
|
|
26
|
+
const input = usage.inputTokens.noCache ??
|
|
27
|
+
Math.max(0, (usage.inputTokens.total ?? 0) - cacheRead - cacheWrite);
|
|
28
|
+
return {
|
|
29
|
+
input,
|
|
30
|
+
output: usage.outputTokens.total ?? 0,
|
|
31
|
+
cacheRead,
|
|
32
|
+
cacheWrite,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function nextOrdinal(map, key) {
|
|
36
|
+
if (!map)
|
|
37
|
+
return 0;
|
|
38
|
+
const n = map.get(key) ?? 0;
|
|
39
|
+
map.set(key, n + 1);
|
|
40
|
+
return n;
|
|
41
|
+
}
|
|
42
|
+
const VOLATILE_BUDGET_PATTERNS = [
|
|
43
|
+
/(budget: )≈\$-?\d[\d,]*(?:\.\d+)?/g,
|
|
44
|
+
/(Remaining research budget: )≈\$-?\d[\d,]*(?:\.\d+)?/g,
|
|
45
|
+
];
|
|
46
|
+
export function normalizeForCacheKey(serialized) {
|
|
47
|
+
let out = serialized;
|
|
48
|
+
for (const pattern of VOLATILE_BUDGET_PATTERNS) {
|
|
49
|
+
out = out.replace(pattern, (_match, label) => `${label}≈$`);
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
function callKey(model, params, role) {
|
|
54
|
+
const material = {
|
|
55
|
+
role,
|
|
56
|
+
provider: model.provider,
|
|
57
|
+
modelId: model.modelId,
|
|
58
|
+
prompt: params.prompt,
|
|
59
|
+
tools: params.tools?.map((tool) => ({
|
|
60
|
+
type: tool.type,
|
|
61
|
+
name: "name" in tool ? tool.name : undefined,
|
|
62
|
+
description: "description" in tool ? tool.description : undefined,
|
|
63
|
+
inputSchema: "inputSchema" in tool ? tool.inputSchema : undefined,
|
|
64
|
+
})),
|
|
65
|
+
toolChoice: params.toolChoice,
|
|
66
|
+
responseFormat: params.responseFormat,
|
|
67
|
+
maxOutputTokens: params.maxOutputTokens,
|
|
68
|
+
};
|
|
69
|
+
return createHash("sha256")
|
|
70
|
+
.update(normalizeForCacheKey(JSON.stringify(material)))
|
|
71
|
+
.digest("hex")
|
|
72
|
+
.slice(0, 40);
|
|
73
|
+
}
|
|
74
|
+
export function createModelCallCache() {
|
|
75
|
+
return new Map();
|
|
76
|
+
}
|
|
77
|
+
function serializeResult(result) {
|
|
78
|
+
const record = {
|
|
79
|
+
content: result.content,
|
|
80
|
+
finishReason: result.finishReason,
|
|
81
|
+
usage: result.usage,
|
|
82
|
+
...(result.providerMetadata
|
|
83
|
+
? { providerMetadata: result.providerMetadata }
|
|
84
|
+
: {}),
|
|
85
|
+
};
|
|
86
|
+
try {
|
|
87
|
+
JSON.stringify(record);
|
|
88
|
+
return record;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function streamFromJournaledCall(record) {
|
|
95
|
+
const parts = [
|
|
96
|
+
{ type: "stream-start", warnings: [] },
|
|
97
|
+
];
|
|
98
|
+
let nextId = 0;
|
|
99
|
+
for (const item of record.content) {
|
|
100
|
+
if (item.type === "text") {
|
|
101
|
+
const id = `replay_${nextId++}`;
|
|
102
|
+
parts.push({ type: "text-start", id }, { type: "text-delta", id, delta: item.text }, { type: "text-end", id });
|
|
103
|
+
}
|
|
104
|
+
else if (item.type === "reasoning") {
|
|
105
|
+
const id = `replay_${nextId++}`;
|
|
106
|
+
parts.push({ type: "reasoning-start", id }, { type: "reasoning-delta", id, delta: item.text }, { type: "reasoning-end", id });
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
parts.push(item);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
parts.push({
|
|
113
|
+
type: "finish",
|
|
114
|
+
usage: record.usage,
|
|
115
|
+
finishReason: record.finishReason,
|
|
116
|
+
...(record.providerMetadata
|
|
117
|
+
? { providerMetadata: record.providerMetadata }
|
|
118
|
+
: {}),
|
|
119
|
+
});
|
|
120
|
+
return new ReadableStream({
|
|
121
|
+
start(controller) {
|
|
122
|
+
for (const part of parts)
|
|
123
|
+
controller.enqueue(part);
|
|
124
|
+
controller.close();
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function collectStreamPart(state, part) {
|
|
129
|
+
switch (part.type) {
|
|
130
|
+
case "text-start":
|
|
131
|
+
state.openText.set(part.id, state.content.push({ type: "text", text: "" }) - 1);
|
|
132
|
+
break;
|
|
133
|
+
case "text-delta": {
|
|
134
|
+
const index = state.openText.get(part.id);
|
|
135
|
+
const entry = index === undefined ? undefined : state.content[index];
|
|
136
|
+
if (entry && entry.type === "text")
|
|
137
|
+
entry.text += part.delta;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case "reasoning-start":
|
|
141
|
+
state.openReasoning.set(part.id, state.content.push({ type: "reasoning", text: "" }) - 1);
|
|
142
|
+
break;
|
|
143
|
+
case "reasoning-delta": {
|
|
144
|
+
const index = state.openReasoning.get(part.id);
|
|
145
|
+
const entry = index === undefined ? undefined : state.content[index];
|
|
146
|
+
if (entry && entry.type === "reasoning")
|
|
147
|
+
entry.text += part.delta;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
case "tool-call":
|
|
151
|
+
case "tool-result":
|
|
152
|
+
case "file":
|
|
153
|
+
case "source":
|
|
154
|
+
state.content.push(part);
|
|
155
|
+
break;
|
|
156
|
+
case "finish":
|
|
157
|
+
state.finish = {
|
|
158
|
+
usage: part.usage,
|
|
159
|
+
finishReason: part.finishReason,
|
|
160
|
+
...(part.providerMetadata
|
|
161
|
+
? { providerMetadata: part.providerMetadata }
|
|
162
|
+
: {}),
|
|
163
|
+
};
|
|
164
|
+
break;
|
|
165
|
+
default:
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function isAnthropicModel(model) {
|
|
170
|
+
return model.provider.toLowerCase().includes("anthropic");
|
|
171
|
+
}
|
|
172
|
+
function isOpenAIModel(model) {
|
|
173
|
+
return model.provider.toLowerCase().includes("openai");
|
|
174
|
+
}
|
|
175
|
+
function withOpenAIDefaults(params) {
|
|
176
|
+
const openai = {
|
|
177
|
+
...params.providerOptions?.openai,
|
|
178
|
+
strictJsonSchema: false,
|
|
179
|
+
};
|
|
180
|
+
return {
|
|
181
|
+
...params,
|
|
182
|
+
providerOptions: { ...params.providerOptions, openai },
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function withCacheBreakpoint(params) {
|
|
186
|
+
if (params.prompt.length === 0)
|
|
187
|
+
return params;
|
|
188
|
+
const prompt = [...params.prompt];
|
|
189
|
+
const last = prompt[prompt.length - 1];
|
|
190
|
+
prompt[prompt.length - 1] = {
|
|
191
|
+
...last,
|
|
192
|
+
providerOptions: {
|
|
193
|
+
...last.providerOptions,
|
|
194
|
+
anthropic: {
|
|
195
|
+
...last.providerOptions?.anthropic,
|
|
196
|
+
cacheControl: { type: "ephemeral" },
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
return { ...params, prompt };
|
|
201
|
+
}
|
|
202
|
+
const RETRY_MAX_ATTEMPTS = 6; // 1 initial try + up to 5 retries
|
|
203
|
+
const RETRY_BASE_DELAY_MS = 1_000;
|
|
204
|
+
const RETRY_MAX_DELAY_MS = 30_000; // cap so a sustained limit can't hang minutes per attempt
|
|
205
|
+
function isAbortError(err) {
|
|
206
|
+
return (err instanceof Error &&
|
|
207
|
+
(err.name === "AbortError" || err.name === "TimeoutError"));
|
|
208
|
+
}
|
|
209
|
+
function parseRetryAfterMs(headers) {
|
|
210
|
+
const raw = headers?.["retry-after"] ?? headers?.["Retry-After"];
|
|
211
|
+
if (!raw)
|
|
212
|
+
return undefined;
|
|
213
|
+
const seconds = Number(raw);
|
|
214
|
+
if (Number.isFinite(seconds))
|
|
215
|
+
return Math.max(0, seconds * 1_000);
|
|
216
|
+
const at = Date.parse(raw);
|
|
217
|
+
if (Number.isFinite(at))
|
|
218
|
+
return Math.max(0, at - Date.now());
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
function unwrapErrors(err) {
|
|
222
|
+
const seen = new Set();
|
|
223
|
+
const queue = [err];
|
|
224
|
+
const out = [];
|
|
225
|
+
while (queue.length > 0 && out.length < 8) {
|
|
226
|
+
const current = queue.shift();
|
|
227
|
+
if (!current || typeof current !== "object" || seen.has(current))
|
|
228
|
+
continue;
|
|
229
|
+
seen.add(current);
|
|
230
|
+
const shape = current;
|
|
231
|
+
out.push(shape);
|
|
232
|
+
if (shape.cause)
|
|
233
|
+
queue.push(shape.cause);
|
|
234
|
+
if (shape.lastError)
|
|
235
|
+
queue.push(shape.lastError);
|
|
236
|
+
if (Array.isArray(shape.errors))
|
|
237
|
+
queue.push(...shape.errors.slice(0, 4));
|
|
238
|
+
}
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
function classifyRetry(err) {
|
|
242
|
+
if (isAbortError(err))
|
|
243
|
+
return { retryable: false };
|
|
244
|
+
const shapes = unwrapErrors(err);
|
|
245
|
+
const retryAfterMs = shapes
|
|
246
|
+
.map((shape) => parseRetryAfterMs(shape.responseHeaders))
|
|
247
|
+
.find((ms) => ms !== undefined);
|
|
248
|
+
const message = shapes
|
|
249
|
+
.map((shape) => shape.message ?? "")
|
|
250
|
+
.join("\n")
|
|
251
|
+
.toLowerCase();
|
|
252
|
+
const rateLimit = shapes.some((shape) => shape.statusCode === 429) ||
|
|
253
|
+
/rate limit|too many requests|overloaded|concurrent connections/.test(message);
|
|
254
|
+
const withDelay = () => ({
|
|
255
|
+
retryable: true,
|
|
256
|
+
...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
|
|
257
|
+
...(rateLimit ? { rateLimit: true } : {}),
|
|
258
|
+
});
|
|
259
|
+
for (const shape of shapes) {
|
|
260
|
+
if (typeof shape.isRetryable === "boolean") {
|
|
261
|
+
return shape.isRetryable ? withDelay() : { retryable: false };
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const retryableStatus = shapes.some((shape) => {
|
|
265
|
+
const status = typeof shape.statusCode === "number" ? shape.statusCode : undefined;
|
|
266
|
+
return (status === 408 ||
|
|
267
|
+
status === 409 ||
|
|
268
|
+
status === 429 ||
|
|
269
|
+
(status !== undefined && status >= 500));
|
|
270
|
+
});
|
|
271
|
+
const retryableMessage = /rate limit|too many requests|overloaded|concurrent connections|timeout|timed out|econnreset|etimedout|eai_again|socket hang up|fetch failed|network error/.test(message);
|
|
272
|
+
return retryableStatus || retryableMessage
|
|
273
|
+
? withDelay()
|
|
274
|
+
: { retryable: false };
|
|
275
|
+
}
|
|
276
|
+
function backoffDelayMs(attempt, retryAfterMs) {
|
|
277
|
+
const exponential = Math.min(RETRY_MAX_DELAY_MS, RETRY_BASE_DELAY_MS * 2 ** (attempt - 1));
|
|
278
|
+
const jittered = exponential / 2 + Math.random() * (exponential / 2);
|
|
279
|
+
return Math.min(RETRY_MAX_DELAY_MS, Math.max(retryAfterMs ?? 0, jittered));
|
|
280
|
+
}
|
|
281
|
+
async function callWithRetry(op, gate, signal, onRateLimit) {
|
|
282
|
+
let tries = 0;
|
|
283
|
+
let release = null;
|
|
284
|
+
try {
|
|
285
|
+
for (;;) {
|
|
286
|
+
tries++;
|
|
287
|
+
if (gate && !release)
|
|
288
|
+
release = await gate.acquire();
|
|
289
|
+
try {
|
|
290
|
+
return await op();
|
|
291
|
+
}
|
|
292
|
+
catch (err) {
|
|
293
|
+
const { retryable, retryAfterMs, rateLimit } = classifyRetry(err);
|
|
294
|
+
if (!retryable || tries >= RETRY_MAX_ATTEMPTS || signal?.aborted)
|
|
295
|
+
throw err;
|
|
296
|
+
const delayMs = backoffDelayMs(tries, retryAfterMs);
|
|
297
|
+
onRateLimit?.({ attempt: tries, delayMs, error: err });
|
|
298
|
+
if (release && !rateLimit) {
|
|
299
|
+
release();
|
|
300
|
+
release = null;
|
|
301
|
+
}
|
|
302
|
+
await sleep(delayMs, signal);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
finally {
|
|
307
|
+
release?.();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
export function engineModel(model, hooks) {
|
|
311
|
+
const inner = model;
|
|
312
|
+
const recorder = hooks.recorder;
|
|
313
|
+
const settle = (usage, hold) => {
|
|
314
|
+
const tokens = tokenUsageFromV3(usage);
|
|
315
|
+
const { pricing, known } = resolvePricing(inner.modelId, hooks.pricing);
|
|
316
|
+
if (!known)
|
|
317
|
+
hooks.onUnknownModel?.(inner.modelId);
|
|
318
|
+
const cost = usageCostUSD(tokens, pricing);
|
|
319
|
+
if (hold)
|
|
320
|
+
hold.settle(cost);
|
|
321
|
+
else
|
|
322
|
+
hooks.grant.charge(cost);
|
|
323
|
+
trackUsage(hooks.usage, hooks.role, tokens);
|
|
324
|
+
hooks.onCost?.(cost);
|
|
325
|
+
return cost;
|
|
326
|
+
};
|
|
327
|
+
const settleReplay = (usage) => {
|
|
328
|
+
const tokens = tokenUsageFromV3(usage);
|
|
329
|
+
const { pricing } = resolvePricing(inner.modelId, hooks.pricing);
|
|
330
|
+
const cost = usageCostUSD(tokens, pricing);
|
|
331
|
+
hooks.grant.charge(cost);
|
|
332
|
+
trackUsage(hooks.usage, hooks.role, tokens);
|
|
333
|
+
hooks.usage.replayedUSD += cost;
|
|
334
|
+
hooks.onCost?.(cost);
|
|
335
|
+
return cost;
|
|
336
|
+
};
|
|
337
|
+
const recordReplayStep = (key, params, cached) => {
|
|
338
|
+
if (!recorder)
|
|
339
|
+
return;
|
|
340
|
+
const at = recorder.now();
|
|
341
|
+
recorder.recordModelCall({
|
|
342
|
+
callKey: key,
|
|
343
|
+
role: hooks.role,
|
|
344
|
+
provider: inner.provider,
|
|
345
|
+
modelId: inner.modelId,
|
|
346
|
+
t0: at,
|
|
347
|
+
t1: at,
|
|
348
|
+
waitMs: 0,
|
|
349
|
+
computeMs: 0,
|
|
350
|
+
tokens: tokenUsageFromV3(cached.usage),
|
|
351
|
+
finishReason: cached.finishReason.unified,
|
|
352
|
+
status: "replayed",
|
|
353
|
+
replayed: true,
|
|
354
|
+
params,
|
|
355
|
+
content: cached.content,
|
|
356
|
+
}, currentFrame());
|
|
357
|
+
};
|
|
358
|
+
const recordCall = (key, params, timing, outcome) => {
|
|
359
|
+
if (!recorder)
|
|
360
|
+
return;
|
|
361
|
+
const tEnd = recorder.now();
|
|
362
|
+
const waitMs = Math.max(0, timing.firstWork - timing.tEnqueue);
|
|
363
|
+
const retryDelayMs = Math.max(0, tEnd - timing.firstWork - timing.computeMs);
|
|
364
|
+
recorder.recordModelCall({
|
|
365
|
+
callKey: key,
|
|
366
|
+
role: hooks.role,
|
|
367
|
+
provider: inner.provider,
|
|
368
|
+
modelId: inner.modelId,
|
|
369
|
+
t0: timing.tEnqueue,
|
|
370
|
+
t1: tEnd,
|
|
371
|
+
waitMs,
|
|
372
|
+
computeMs: timing.computeMs,
|
|
373
|
+
...(retryDelayMs ? { retryDelayMs } : {}),
|
|
374
|
+
...(outcome.status === "ok"
|
|
375
|
+
? {
|
|
376
|
+
tokens: tokenUsageFromV3(outcome.usage),
|
|
377
|
+
costUSD: outcome.costUSD,
|
|
378
|
+
finishReason: outcome.finishReason,
|
|
379
|
+
status: "ok",
|
|
380
|
+
...(outcome.content ? { content: outcome.content } : {}),
|
|
381
|
+
}
|
|
382
|
+
: { status: outcome.status, error: outcome.error }),
|
|
383
|
+
params,
|
|
384
|
+
}, currentFrame());
|
|
385
|
+
};
|
|
386
|
+
const reserveFor = (params) => {
|
|
387
|
+
const { pricing } = resolvePricing(inner.modelId, hooks.pricing);
|
|
388
|
+
let promptChars = 0;
|
|
389
|
+
try {
|
|
390
|
+
promptChars = JSON.stringify(params.prompt).length;
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
promptChars = 0;
|
|
394
|
+
}
|
|
395
|
+
const estimateUSD = usageCostUSD({
|
|
396
|
+
input: promptChars / ECONOMY.callReserve.promptCharsPerToken,
|
|
397
|
+
output: params.maxOutputTokens ?? ECONOMY.callReserve.assumedOutputTokens,
|
|
398
|
+
cacheRead: 0,
|
|
399
|
+
cacheWrite: 0,
|
|
400
|
+
}, pricing);
|
|
401
|
+
const hold = hooks.grant.reserve(estimateUSD);
|
|
402
|
+
return hold ? { hold, estimateUSD } : null;
|
|
403
|
+
};
|
|
404
|
+
const settleFailure = (reservation, err, abortSignal) => {
|
|
405
|
+
if (!reservation)
|
|
406
|
+
return;
|
|
407
|
+
if (isAbortError(err) || abortSignal?.aborted) {
|
|
408
|
+
reservation.hold.settle(reservation.estimateUSD);
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
reservation.hold.release();
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
const middleware = {
|
|
415
|
+
specificationVersion: "v3",
|
|
416
|
+
transformParams: async ({ params }) => {
|
|
417
|
+
if (isAnthropicModel(inner))
|
|
418
|
+
return withCacheBreakpoint(params);
|
|
419
|
+
if (isOpenAIModel(inner))
|
|
420
|
+
return withOpenAIDefaults(params);
|
|
421
|
+
return params;
|
|
422
|
+
},
|
|
423
|
+
wrapGenerate: async ({ doGenerate, params }) => {
|
|
424
|
+
const key = callKey(inner, params, hooks.role);
|
|
425
|
+
const journalKey = `${key}#${nextOrdinal(hooks.callOrdinals, key)}`;
|
|
426
|
+
const cached = hooks.replay?.take(journalKey);
|
|
427
|
+
if (cached) {
|
|
428
|
+
settleReplay(cached.usage);
|
|
429
|
+
recordReplayStep(key, params, cached);
|
|
430
|
+
hooks.modelCache?.set(key, cached);
|
|
431
|
+
return {
|
|
432
|
+
content: cached.content,
|
|
433
|
+
finishReason: cached.finishReason,
|
|
434
|
+
usage: cached.usage,
|
|
435
|
+
...(cached.providerMetadata
|
|
436
|
+
? { providerMetadata: cached.providerMetadata }
|
|
437
|
+
: {}),
|
|
438
|
+
warnings: [],
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
const reused = hooks.modelCache?.get(key);
|
|
442
|
+
if (reused) {
|
|
443
|
+
recordReplayStep(key, params, reused);
|
|
444
|
+
hooks.onCacheHit?.();
|
|
445
|
+
return {
|
|
446
|
+
content: reused.content,
|
|
447
|
+
finishReason: reused.finishReason,
|
|
448
|
+
usage: reused.usage,
|
|
449
|
+
...(reused.providerMetadata
|
|
450
|
+
? { providerMetadata: reused.providerMetadata }
|
|
451
|
+
: {}),
|
|
452
|
+
warnings: [],
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
if (hooks.budgetExhausted?.()) {
|
|
456
|
+
throw new AtlasError("budget exhausted: total run budget reached", "budget");
|
|
457
|
+
}
|
|
458
|
+
const reservation = reserveFor(params);
|
|
459
|
+
const timing = {
|
|
460
|
+
tEnqueue: recorder ? recorder.now() : 0,
|
|
461
|
+
firstWork: 0,
|
|
462
|
+
computeMs: 0,
|
|
463
|
+
};
|
|
464
|
+
let acquired = false;
|
|
465
|
+
let result;
|
|
466
|
+
try {
|
|
467
|
+
result = await callWithRetry(() => {
|
|
468
|
+
if (!recorder)
|
|
469
|
+
return Promise.resolve(doGenerate());
|
|
470
|
+
const s = recorder.now();
|
|
471
|
+
if (!acquired) {
|
|
472
|
+
timing.firstWork = s;
|
|
473
|
+
acquired = true;
|
|
474
|
+
}
|
|
475
|
+
return Promise.resolve(doGenerate()).then((r) => {
|
|
476
|
+
timing.computeMs += recorder.now() - s;
|
|
477
|
+
return r;
|
|
478
|
+
}, (e) => {
|
|
479
|
+
timing.computeMs += recorder.now() - s;
|
|
480
|
+
throw e;
|
|
481
|
+
});
|
|
482
|
+
}, hooks.gate, params.abortSignal, hooks.onRateLimit);
|
|
483
|
+
}
|
|
484
|
+
catch (err) {
|
|
485
|
+
settleFailure(reservation, err, params.abortSignal);
|
|
486
|
+
recordCall(key, params, timing, {
|
|
487
|
+
status: isAbortError(err) || params.abortSignal?.aborted
|
|
488
|
+
? "aborted"
|
|
489
|
+
: "error",
|
|
490
|
+
error: errorMessage(err),
|
|
491
|
+
});
|
|
492
|
+
throw err;
|
|
493
|
+
}
|
|
494
|
+
const costUSD = settle(result.usage, reservation?.hold ?? null);
|
|
495
|
+
if (hooks.journal || hooks.modelCache) {
|
|
496
|
+
const record = serializeResult(result);
|
|
497
|
+
if (record) {
|
|
498
|
+
hooks.journal?.call(journalKey, record);
|
|
499
|
+
hooks.modelCache?.set(key, record);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
recordCall(key, params, timing, {
|
|
503
|
+
status: "ok",
|
|
504
|
+
usage: result.usage,
|
|
505
|
+
finishReason: result.finishReason.unified,
|
|
506
|
+
costUSD,
|
|
507
|
+
content: result.content,
|
|
508
|
+
});
|
|
509
|
+
return result;
|
|
510
|
+
},
|
|
511
|
+
wrapStream: async ({ doStream, params }) => {
|
|
512
|
+
const key = callKey(inner, params, hooks.role);
|
|
513
|
+
const journalKey = `${key}#${nextOrdinal(hooks.callOrdinals, key)}`;
|
|
514
|
+
const cached = hooks.replay?.take(journalKey);
|
|
515
|
+
if (cached) {
|
|
516
|
+
settleReplay(cached.usage);
|
|
517
|
+
recordReplayStep(key, params, cached);
|
|
518
|
+
hooks.modelCache?.set(key, cached);
|
|
519
|
+
return { stream: streamFromJournaledCall(cached) };
|
|
520
|
+
}
|
|
521
|
+
if (hooks.budgetExhausted?.()) {
|
|
522
|
+
throw new AtlasError("budget exhausted: total run budget reached", "budget");
|
|
523
|
+
}
|
|
524
|
+
const reservation = reserveFor(params);
|
|
525
|
+
const tEnqueue = recorder ? recorder.now() : 0;
|
|
526
|
+
let firstWork = tEnqueue;
|
|
527
|
+
const releaseSlot = await hooks.gate.acquire();
|
|
528
|
+
let result;
|
|
529
|
+
try {
|
|
530
|
+
result = await callWithRetry(() => {
|
|
531
|
+
if (recorder)
|
|
532
|
+
firstWork = recorder.now();
|
|
533
|
+
return Promise.resolve(doStream());
|
|
534
|
+
}, undefined, params.abortSignal, hooks.onRateLimit);
|
|
535
|
+
}
|
|
536
|
+
catch (err) {
|
|
537
|
+
releaseSlot();
|
|
538
|
+
settleFailure(reservation, err, params.abortSignal);
|
|
539
|
+
if (recorder) {
|
|
540
|
+
recordCall(key, params, {
|
|
541
|
+
tEnqueue,
|
|
542
|
+
firstWork,
|
|
543
|
+
computeMs: Math.max(0, recorder.now() - firstWork),
|
|
544
|
+
}, {
|
|
545
|
+
status: isAbortError(err) || params.abortSignal?.aborted
|
|
546
|
+
? "aborted"
|
|
547
|
+
: "error",
|
|
548
|
+
error: errorMessage(err),
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
throw err;
|
|
552
|
+
}
|
|
553
|
+
const state = {
|
|
554
|
+
content: [],
|
|
555
|
+
openText: new Map(),
|
|
556
|
+
openReasoning: new Map(),
|
|
557
|
+
};
|
|
558
|
+
let lastCost;
|
|
559
|
+
let recorded = false;
|
|
560
|
+
const recordStream = (status) => {
|
|
561
|
+
if (!recorder || recorded)
|
|
562
|
+
return;
|
|
563
|
+
recorded = true;
|
|
564
|
+
const tEnd = recorder.now();
|
|
565
|
+
recorder.recordModelCall({
|
|
566
|
+
callKey: key,
|
|
567
|
+
role: hooks.role,
|
|
568
|
+
provider: inner.provider,
|
|
569
|
+
modelId: inner.modelId,
|
|
570
|
+
t0: tEnqueue,
|
|
571
|
+
t1: tEnd,
|
|
572
|
+
waitMs: Math.max(0, firstWork - tEnqueue),
|
|
573
|
+
computeMs: Math.max(0, tEnd - firstWork),
|
|
574
|
+
...(state.finish
|
|
575
|
+
? { tokens: tokenUsageFromV3(state.finish.usage) }
|
|
576
|
+
: {}),
|
|
577
|
+
...(lastCost !== undefined ? { costUSD: lastCost } : {}),
|
|
578
|
+
...(state.finish?.finishReason
|
|
579
|
+
? { finishReason: state.finish.finishReason.unified }
|
|
580
|
+
: {}),
|
|
581
|
+
status,
|
|
582
|
+
params,
|
|
583
|
+
content: state.content,
|
|
584
|
+
}, currentFrame());
|
|
585
|
+
};
|
|
586
|
+
const metered = result.stream.pipeThrough(new TransformStream({
|
|
587
|
+
transform(part, controller) {
|
|
588
|
+
collectStreamPart(state, part);
|
|
589
|
+
if (part.type === "finish") {
|
|
590
|
+
lastCost = settle(part.usage, reservation?.hold ?? null);
|
|
591
|
+
releaseSlot();
|
|
592
|
+
}
|
|
593
|
+
controller.enqueue(part);
|
|
594
|
+
},
|
|
595
|
+
cancel() {
|
|
596
|
+
if (reservation && !state.finish) {
|
|
597
|
+
reservation.hold.settle(reservation.estimateUSD);
|
|
598
|
+
}
|
|
599
|
+
reservation?.hold.release();
|
|
600
|
+
releaseSlot();
|
|
601
|
+
recordStream("aborted");
|
|
602
|
+
},
|
|
603
|
+
flush() {
|
|
604
|
+
if (reservation && !state.finish) {
|
|
605
|
+
reservation.hold.settle(reservation.estimateUSD);
|
|
606
|
+
}
|
|
607
|
+
reservation?.hold.release();
|
|
608
|
+
releaseSlot();
|
|
609
|
+
if (state.finish && hooks.journal) {
|
|
610
|
+
const record = serializeResult({
|
|
611
|
+
content: state.content,
|
|
612
|
+
finishReason: state.finish.finishReason,
|
|
613
|
+
usage: state.finish.usage,
|
|
614
|
+
...(state.finish.providerMetadata
|
|
615
|
+
? { providerMetadata: state.finish.providerMetadata }
|
|
616
|
+
: {}),
|
|
617
|
+
warnings: [],
|
|
618
|
+
});
|
|
619
|
+
if (record)
|
|
620
|
+
hooks.journal.call(journalKey, record);
|
|
621
|
+
}
|
|
622
|
+
recordStream(state.finish ? "ok" : "aborted");
|
|
623
|
+
},
|
|
624
|
+
}));
|
|
625
|
+
return { ...result, stream: metered };
|
|
626
|
+
},
|
|
627
|
+
};
|
|
628
|
+
return wrapLanguageModel({ model: inner, middleware });
|
|
629
|
+
}
|
|
630
|
+
export const MODEL_CALL_MAX_RETRIES = 0;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Researcher } from "./researcher.js";
|
|
2
|
+
import { type SpineOutput } from "./spine.js";
|
|
3
|
+
import type { RunCtx } from "./state.js";
|
|
4
|
+
export declare const ATLAS_KEY = "atlas";
|
|
5
|
+
export declare function runOrchestrated(rctx: RunCtx, researchers: Record<string, Researcher>): Promise<SpineOutput>;
|