@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.
Files changed (112) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +219 -0
  3. package/dist/agent.d.ts +34 -0
  4. package/dist/agent.js +133 -0
  5. package/dist/async.d.ts +19 -0
  6. package/dist/async.js +172 -0
  7. package/dist/atlas.d.ts +19 -0
  8. package/dist/atlas.js +69 -0
  9. package/dist/budget.d.ts +64 -0
  10. package/dist/budget.js +336 -0
  11. package/dist/checklist.d.ts +115 -0
  12. package/dist/checklist.js +297 -0
  13. package/dist/cli.js +38700 -0
  14. package/dist/config.d.ts +80 -0
  15. package/dist/config.js +109 -0
  16. package/dist/context.d.ts +26 -0
  17. package/dist/context.js +250 -0
  18. package/dist/custom-tools.d.ts +26 -0
  19. package/dist/custom-tools.js +33 -0
  20. package/dist/defaults.d.ts +10 -0
  21. package/dist/defaults.js +37 -0
  22. package/dist/economy.d.ts +12 -0
  23. package/dist/economy.js +6 -0
  24. package/dist/env.d.ts +1 -0
  25. package/dist/env.js +8 -0
  26. package/dist/errors.d.ts +6 -0
  27. package/dist/errors.js +11 -0
  28. package/dist/event-hub.d.ts +11 -0
  29. package/dist/event-hub.js +83 -0
  30. package/dist/events.d.ts +105 -0
  31. package/dist/events.js +1 -0
  32. package/dist/html-extract.d.ts +21 -0
  33. package/dist/html-extract.js +459 -0
  34. package/dist/index.d.ts +59 -0
  35. package/dist/index.js +26 -0
  36. package/dist/memory.d.ts +2 -0
  37. package/dist/memory.js +38 -0
  38. package/dist/model.d.ts +49 -0
  39. package/dist/model.js +630 -0
  40. package/dist/orchestrate.d.ts +5 -0
  41. package/dist/orchestrate.js +277 -0
  42. package/dist/pdf-extract.d.ts +5 -0
  43. package/dist/pdf-extract.js +20 -0
  44. package/dist/prompts.d.ts +2 -0
  45. package/dist/prompts.js +6 -0
  46. package/dist/providers/domain/arxiv.d.ts +6 -0
  47. package/dist/providers/domain/arxiv.js +83 -0
  48. package/dist/providers/domain/clinicaltrials.d.ts +6 -0
  49. package/dist/providers/domain/clinicaltrials.js +104 -0
  50. package/dist/providers/domain/edgar.d.ts +10 -0
  51. package/dist/providers/domain/edgar.js +92 -0
  52. package/dist/providers/domain/index.d.ts +14 -0
  53. package/dist/providers/domain/index.js +7 -0
  54. package/dist/providers/domain/openalex.d.ts +7 -0
  55. package/dist/providers/domain/openalex.js +128 -0
  56. package/dist/providers/domain/pubmed.d.ts +8 -0
  57. package/dist/providers/domain/pubmed.js +123 -0
  58. package/dist/providers/domain/semantic-scholar.d.ts +6 -0
  59. package/dist/providers/domain/semantic-scholar.js +112 -0
  60. package/dist/providers/domain/shared.d.ts +12 -0
  61. package/dist/providers/domain/shared.js +39 -0
  62. package/dist/providers/domain/wikipedia.d.ts +6 -0
  63. package/dist/providers/domain/wikipedia.js +71 -0
  64. package/dist/providers/exa-agent.d.ts +9 -0
  65. package/dist/providers/exa-agent.js +67 -0
  66. package/dist/providers/fetch.d.ts +66 -0
  67. package/dist/providers/fetch.js +675 -0
  68. package/dist/providers/parallel-agent.d.ts +11 -0
  69. package/dist/providers/parallel-agent.js +100 -0
  70. package/dist/providers/perplexity-agent.d.ts +17 -0
  71. package/dist/providers/perplexity-agent.js +86 -0
  72. package/dist/providers/search.d.ts +65 -0
  73. package/dist/providers/search.js +433 -0
  74. package/dist/providers/store.d.ts +48 -0
  75. package/dist/providers/store.js +217 -0
  76. package/dist/researcher.d.ts +20 -0
  77. package/dist/researcher.js +3 -0
  78. package/dist/robots.d.ts +16 -0
  79. package/dist/robots.js +146 -0
  80. package/dist/roles.d.ts +6 -0
  81. package/dist/roles.js +4 -0
  82. package/dist/run.d.ts +65 -0
  83. package/dist/run.js +371 -0
  84. package/dist/safe-dispatcher.d.ts +16 -0
  85. package/dist/safe-dispatcher.js +32 -0
  86. package/dist/safety.d.ts +23 -0
  87. package/dist/safety.js +206 -0
  88. package/dist/sandbox.d.ts +22 -0
  89. package/dist/sandbox.js +228 -0
  90. package/dist/search-normalize.d.ts +2 -0
  91. package/dist/search-normalize.js +13 -0
  92. package/dist/source-documents.d.ts +77 -0
  93. package/dist/source-documents.js +421 -0
  94. package/dist/sources.d.ts +57 -0
  95. package/dist/sources.js +1 -0
  96. package/dist/spine.d.ts +19 -0
  97. package/dist/spine.js +722 -0
  98. package/dist/state.d.ts +90 -0
  99. package/dist/state.js +27 -0
  100. package/dist/structured.d.ts +7 -0
  101. package/dist/structured.js +18 -0
  102. package/dist/tools.d.ts +33 -0
  103. package/dist/tools.js +1187 -0
  104. package/dist/trace-digest.d.ts +11 -0
  105. package/dist/trace-digest.js +309 -0
  106. package/dist/trace.d.ts +225 -0
  107. package/dist/trace.js +278 -0
  108. package/dist/trail.d.ts +15 -0
  109. package/dist/trail.js +74 -0
  110. package/dist/url.d.ts +1 -0
  111. package/dist/url.js +25 -0
  112. package/package.json +107 -0
@@ -0,0 +1,433 @@
1
+ import { generateText } from "ai";
2
+ import { sleep } from "../async.js";
3
+ import { isZaiModelId } from "../defaults.js";
4
+ import { readEnv } from "../env.js";
5
+ import { errorMessage } from "../errors.js";
6
+ import { normalizeUrlForSource } from "../url.js";
7
+ export function safeDomain(url) {
8
+ try {
9
+ return new URL(url).hostname.replace(/^www\./, "");
10
+ }
11
+ catch {
12
+ return url;
13
+ }
14
+ }
15
+ function toResult(index, url, title, snippet) {
16
+ if (typeof url !== "string" || !/^https?:\/\//i.test(url))
17
+ return null;
18
+ return {
19
+ position: index + 1,
20
+ title: typeof title === "string" && title.trim() ? title.trim() : url,
21
+ url,
22
+ snippet,
23
+ domain: safeDomain(url),
24
+ };
25
+ }
26
+ async function readErrorBody(resp) {
27
+ try {
28
+ const text = (await resp.text()).trim();
29
+ return text.length > 200 ? `${text.slice(0, 200)}…` : text;
30
+ }
31
+ catch {
32
+ return "";
33
+ }
34
+ }
35
+ class SearchProviderError extends Error {
36
+ statusCode;
37
+ retryAfterMs;
38
+ constructor(message, statusCode, retryAfterMs) {
39
+ super(message);
40
+ this.name = "SearchProviderError";
41
+ this.statusCode = statusCode;
42
+ if (retryAfterMs !== undefined)
43
+ this.retryAfterMs = retryAfterMs;
44
+ }
45
+ }
46
+ function parseRetryAfterMs(headers) {
47
+ const raw = headers.get("retry-after");
48
+ if (!raw)
49
+ return undefined;
50
+ const seconds = Number(raw);
51
+ if (Number.isFinite(seconds))
52
+ return Math.max(0, seconds * 1_000);
53
+ const at = Date.parse(raw);
54
+ if (Number.isFinite(at))
55
+ return Math.max(0, at - Date.now());
56
+ return undefined;
57
+ }
58
+ async function searchHttpError(label, resp) {
59
+ const body = await readErrorBody(resp);
60
+ return new SearchProviderError(`${label}: HTTP ${resp.status}: ${body}`, resp.status, parseRetryAfterMs(resp.headers));
61
+ }
62
+ const SEARCH_RETRY_MAX_ATTEMPTS = 5;
63
+ const SEARCH_RETRY_BASE_DELAY_MS = 500;
64
+ const SEARCH_RETRY_MAX_DELAY_MS = 15_000;
65
+ function classifySearchRetry(err) {
66
+ if (err instanceof Error &&
67
+ (err.name === "AbortError" || err.name === "TimeoutError")) {
68
+ return { retryable: false };
69
+ }
70
+ if (err instanceof SearchProviderError) {
71
+ const status = err.statusCode;
72
+ const retryable = status === 408 || status === 409 || status === 429 || status >= 500;
73
+ if (!retryable)
74
+ return { retryable: false };
75
+ return {
76
+ retryable: true,
77
+ ...(err.retryAfterMs !== undefined
78
+ ? { retryAfterMs: err.retryAfterMs }
79
+ : {}),
80
+ };
81
+ }
82
+ const message = (err instanceof Error ? err.message : String(err)).toLowerCase();
83
+ return /rate limit|too many requests|overloaded|concurrent connections|timeout|timed out|econnreset|etimedout|eai_again|socket hang up|fetch failed|network error/.test(message)
84
+ ? { retryable: true }
85
+ : { retryable: false };
86
+ }
87
+ function searchBackoffDelayMs(attempt, retryAfterMs) {
88
+ const exponential = Math.min(SEARCH_RETRY_MAX_DELAY_MS, SEARCH_RETRY_BASE_DELAY_MS * 2 ** (attempt - 1));
89
+ const jittered = exponential / 2 + Math.random() * (exponential / 2);
90
+ return Math.min(SEARCH_RETRY_MAX_DELAY_MS, Math.max(retryAfterMs ?? 0, jittered));
91
+ }
92
+ async function searchWithRetry(provider, q) {
93
+ let tries = 0;
94
+ for (;;) {
95
+ tries++;
96
+ try {
97
+ return await provider.search(q);
98
+ }
99
+ catch (err) {
100
+ const { retryable, retryAfterMs } = classifySearchRetry(err);
101
+ if (!retryable ||
102
+ tries >= SEARCH_RETRY_MAX_ATTEMPTS ||
103
+ q.signal?.aborted) {
104
+ throw err;
105
+ }
106
+ await sleep(searchBackoffDelayMs(tries, retryAfterMs), q.signal);
107
+ }
108
+ }
109
+ }
110
+ function clampLimit(limit, max) {
111
+ return Math.min(Math.max(1, Math.floor(limit ?? 8)), max);
112
+ }
113
+ export function tavily(opts = {}) {
114
+ const apiKey = opts.apiKey ?? readEnv("ATLAS_TAVILY_API_KEY", "TAVILY_API_KEY");
115
+ if (!apiKey) {
116
+ throw new Error("tavily() requires an apiKey (or set ATLAS_TAVILY_API_KEY / TAVILY_API_KEY)");
117
+ }
118
+ const endpoint = `${(opts.baseUrl ?? "https://api.tavily.com").replace(/\/+$/, "")}/search`;
119
+ return {
120
+ id: "tavily",
121
+ async search({ query, maxResults, signal }) {
122
+ const resp = await fetch(endpoint, {
123
+ method: "POST",
124
+ signal: signal ?? null,
125
+ headers: {
126
+ "content-type": "application/json",
127
+ authorization: `Bearer ${apiKey}`,
128
+ },
129
+ body: JSON.stringify({
130
+ query,
131
+ max_results: clampLimit(maxResults, 20),
132
+ }),
133
+ });
134
+ if (!resp.ok) {
135
+ throw await searchHttpError("tavily", resp);
136
+ }
137
+ const data = (await resp.json());
138
+ const rows = Array.isArray(data.results) ? data.results : [];
139
+ const results = [];
140
+ for (const row of rows) {
141
+ if (!row || typeof row !== "object")
142
+ continue;
143
+ const record = row;
144
+ const snippet = typeof record.content === "string"
145
+ ? record.content.slice(0, 500).trim()
146
+ : "";
147
+ const result = toResult(results.length, record.url, record.title, snippet);
148
+ if (result)
149
+ results.push(result);
150
+ }
151
+ return results;
152
+ },
153
+ };
154
+ }
155
+ export function exa(opts = {}) {
156
+ const apiKey = opts.apiKey ?? readEnv("ATLAS_EXA_API_KEY", "EXA_API_KEY");
157
+ if (!apiKey) {
158
+ throw new Error("exa() requires an apiKey (or set ATLAS_EXA_API_KEY / EXA_API_KEY)");
159
+ }
160
+ const endpoint = `${(opts.baseUrl ?? "https://api.exa.ai").replace(/\/+$/, "")}/search`;
161
+ return {
162
+ id: "exa",
163
+ async search({ query, maxResults, signal }) {
164
+ const resp = await fetch(endpoint, {
165
+ method: "POST",
166
+ signal: signal ?? null,
167
+ headers: {
168
+ "content-type": "application/json",
169
+ "x-api-key": apiKey,
170
+ },
171
+ body: JSON.stringify({
172
+ query,
173
+ numResults: clampLimit(maxResults, 100),
174
+ ...(opts.type ? { type: opts.type } : {}),
175
+ contents: { highlights: { numSentences: 3, highlightsPerUrl: 2 } },
176
+ }),
177
+ });
178
+ if (!resp.ok) {
179
+ throw await searchHttpError("exa", resp);
180
+ }
181
+ const data = (await resp.json());
182
+ const rows = Array.isArray(data.results) ? data.results : [];
183
+ const results = [];
184
+ for (const row of rows) {
185
+ if (!row || typeof row !== "object")
186
+ continue;
187
+ const record = row;
188
+ const highlights = Array.isArray(record.highlights)
189
+ ? record.highlights.filter((h) => typeof h === "string")
190
+ : [];
191
+ const snippet = highlights.join(" … ").trim() ||
192
+ (typeof record.text === "string"
193
+ ? record.text.slice(0, 500).trim()
194
+ : "");
195
+ const result = toResult(results.length, record.url, record.title, snippet);
196
+ if (result)
197
+ results.push(result);
198
+ }
199
+ return results;
200
+ },
201
+ };
202
+ }
203
+ export function brave(opts = {}) {
204
+ const apiKey = opts.apiKey ?? readEnv("ATLAS_BRAVE_API_KEY", "BRAVE_API_KEY");
205
+ if (!apiKey) {
206
+ throw new Error("brave() requires an apiKey (or set ATLAS_BRAVE_API_KEY / BRAVE_API_KEY)");
207
+ }
208
+ const base = `${(opts.baseUrl ?? "https://api.search.brave.com").replace(/\/+$/, "")}/res/v1/web/search`;
209
+ return {
210
+ id: "brave",
211
+ async search({ query, maxResults, signal }) {
212
+ const params = new URLSearchParams({
213
+ q: query,
214
+ count: String(clampLimit(maxResults, 20)),
215
+ extra_snippets: "true",
216
+ });
217
+ if (opts.country)
218
+ params.set("country", opts.country);
219
+ if (opts.searchLang)
220
+ params.set("search_lang", opts.searchLang);
221
+ const resp = await fetch(`${base}?${params.toString()}`, {
222
+ signal: signal ?? null,
223
+ headers: {
224
+ accept: "application/json",
225
+ "x-subscription-token": apiKey,
226
+ },
227
+ });
228
+ if (!resp.ok) {
229
+ throw await searchHttpError("brave", resp);
230
+ }
231
+ const data = (await resp.json());
232
+ const rows = Array.isArray(data.web?.results) ? data.web.results : [];
233
+ const results = [];
234
+ for (const row of rows) {
235
+ if (!row || typeof row !== "object")
236
+ continue;
237
+ const record = row;
238
+ const extra = Array.isArray(record.extra_snippets)
239
+ ? record.extra_snippets.filter((s) => typeof s === "string")
240
+ : [];
241
+ const snippet = (typeof record.description === "string" ? record.description : "") ||
242
+ extra.join(" … ");
243
+ const result = toResult(results.length, record.url, record.title, snippet.trim());
244
+ if (result)
245
+ results.push(result);
246
+ }
247
+ return results;
248
+ },
249
+ };
250
+ }
251
+ export function nativeModelSearch(opts) {
252
+ return {
253
+ id: "model-native",
254
+ async search({ query, maxResults, signal }) {
255
+ const tools = await nativeSearchTools(opts.model);
256
+ const limit = clampLimit(maxResults, 10);
257
+ const result = await generateText({
258
+ model: opts.model,
259
+ prompt: `Search the web for: ${query}\n\n` +
260
+ `Run one web search and list the ${limit} most relevant distinct result pages. ` +
261
+ "Do not answer the question; just surface the sources. " +
262
+ "Write one line per result in the form `<plain url> :: <one-sentence summary of what the page contains>`, with no markdown links.",
263
+ tools,
264
+ maxOutputTokens: 1_500,
265
+ abortSignal: signal,
266
+ });
267
+ const snippets = snippetsByUrl(result.text);
268
+ const seen = new Set();
269
+ const results = [];
270
+ for (const source of result.sources) {
271
+ if (source.sourceType !== "url")
272
+ continue;
273
+ const key = normalizeUrlForSource(source.url);
274
+ if (seen.has(key))
275
+ continue;
276
+ seen.add(key);
277
+ const parsed = toResult(results.length, source.url, source.title, snippets.get(key) ?? "");
278
+ if (parsed)
279
+ results.push(parsed);
280
+ if (results.length >= limit)
281
+ break;
282
+ }
283
+ return results;
284
+ },
285
+ };
286
+ }
287
+ function snippetsByUrl(text) {
288
+ const map = new Map();
289
+ for (const line of text.split("\n")) {
290
+ const match = /(https?:\/\/\S+?)[)\]>.,]*\s*::\s*(\S.*)/.exec(line);
291
+ if (!match)
292
+ continue;
293
+ const key = normalizeUrlForSource(match[1]);
294
+ if (!map.has(key))
295
+ map.set(key, match[2].trim().slice(0, 500));
296
+ }
297
+ return map;
298
+ }
299
+ async function importProvider(pkg, load) {
300
+ try {
301
+ return await load();
302
+ }
303
+ catch (err) {
304
+ throw new Error(`nativeModelSearch needs "${pkg}" installed for this provider's server-side search; ` +
305
+ `install it (npm install ${pkg}) or configure a search adapter (tavily(), exa(), brave()). ` +
306
+ `Original error: ${errorMessage(err)}`);
307
+ }
308
+ }
309
+ async function nativeSearchTools(model) {
310
+ const provider = model.provider.toLowerCase();
311
+ if (isZaiModelId(model.modelId)) {
312
+ throw new Error(`nativeModelSearch: provider "${model.provider}" has no known server-side search tool; configure a search adapter (tavily(), exa(), brave())`);
313
+ }
314
+ if (provider.includes("anthropic")) {
315
+ const { anthropic } = await importProvider("@ai-sdk/anthropic", () => import("@ai-sdk/anthropic"));
316
+ return {
317
+ web_search: anthropic.tools.webSearch_20250305({ maxUses: 1 }),
318
+ };
319
+ }
320
+ if (provider.includes("openai")) {
321
+ const { openai } = await importProvider("@ai-sdk/openai", () => import("@ai-sdk/openai"));
322
+ return { web_search: openai.tools.webSearch({}) };
323
+ }
324
+ if (provider.includes("google")) {
325
+ const { google } = await importProvider("@ai-sdk/google", () => import("@ai-sdk/google"));
326
+ return { google_search: google.tools.googleSearch({}) };
327
+ }
328
+ throw new Error(`nativeModelSearch: provider "${model.provider}" has no known server-side search tool; configure a search adapter (tavily(), exa(), brave())`);
329
+ }
330
+ const RRF_K = 60;
331
+ export function openUrlsOf(meta) {
332
+ const raw = meta?.openUrls;
333
+ if (!Array.isArray(raw))
334
+ return [];
335
+ const out = [];
336
+ for (const url of raw) {
337
+ if (typeof url === "string" && /^https?:\/\//i.test(url))
338
+ out.push(url);
339
+ }
340
+ return out;
341
+ }
342
+ function mergeMeta(existing, incoming, preferIncoming) {
343
+ const openUrls = [
344
+ ...new Set([...openUrlsOf(existing), ...openUrlsOf(incoming)]),
345
+ ];
346
+ const base = preferIncoming
347
+ ? { ...existing, ...incoming }
348
+ : { ...incoming, ...existing };
349
+ if (openUrls.length === 0) {
350
+ return Object.keys(base).length > 0 ? base : undefined;
351
+ }
352
+ return { ...base, openUrls };
353
+ }
354
+ export function mergeSearchResults(lists, limit) {
355
+ const byUrl = new Map();
356
+ for (const list of lists) {
357
+ for (const result of list.results) {
358
+ const key = normalizeUrlForSource(result.url);
359
+ const score = 1 / (RRF_K + result.position);
360
+ const existing = byUrl.get(key);
361
+ if (!existing) {
362
+ byUrl.set(key, {
363
+ title: result.title,
364
+ url: result.url,
365
+ snippet: result.snippet,
366
+ provider: list.provider,
367
+ providerRank: result.position,
368
+ providers: [list.provider],
369
+ score,
370
+ ...(result.meta ? { meta: result.meta } : {}),
371
+ });
372
+ continue;
373
+ }
374
+ existing.score += score;
375
+ if (!existing.providers.includes(list.provider)) {
376
+ existing.providers.push(list.provider);
377
+ }
378
+ const betterRank = result.position < existing.providerRank;
379
+ if (betterRank) {
380
+ existing.title = result.title;
381
+ existing.url = result.url;
382
+ existing.snippet = result.snippet || existing.snippet;
383
+ existing.provider = list.provider;
384
+ existing.providerRank = result.position;
385
+ }
386
+ const meta = mergeMeta(existing.meta, result.meta, betterRank);
387
+ if (meta)
388
+ existing.meta = meta;
389
+ }
390
+ }
391
+ return [...byUrl.values()]
392
+ .sort((a, b) => b.score - a.score || a.providerRank - b.providerRank)
393
+ .slice(0, limit);
394
+ }
395
+ export function combineSearchProviders(providers) {
396
+ return {
397
+ providers,
398
+ async run(q) {
399
+ const warnings = [];
400
+ const lists = await Promise.all(providers.map(async (provider) => {
401
+ try {
402
+ return {
403
+ provider: provider.id,
404
+ results: await searchWithRetry(provider, q),
405
+ };
406
+ }
407
+ catch (err) {
408
+ warnings.push(`${provider.id}: ${errorMessage(err)}`);
409
+ return { provider: provider.id, results: [] };
410
+ }
411
+ }));
412
+ return {
413
+ merged: mergeSearchResults(lists, clampLimit(q.maxResults, 20)),
414
+ warnings,
415
+ };
416
+ },
417
+ };
418
+ }
419
+ export function defaultSearchProviders(model) {
420
+ const providers = [];
421
+ if (readEnv("ATLAS_TAVILY_API_KEY", "TAVILY_API_KEY")) {
422
+ providers.push(tavily());
423
+ }
424
+ if (readEnv("ATLAS_EXA_API_KEY", "EXA_API_KEY")) {
425
+ providers.push(exa());
426
+ }
427
+ if (readEnv("ATLAS_BRAVE_API_KEY", "BRAVE_API_KEY")) {
428
+ providers.push(brave());
429
+ }
430
+ if (providers.length > 0)
431
+ return providers;
432
+ return [nativeModelSearch({ model })];
433
+ }
@@ -0,0 +1,48 @@
1
+ import { type RunTrace } from "../trace.js";
2
+ export interface JournalEntry {
3
+ seq: number;
4
+ kind: "meta" | "event" | "call" | "io" | "trace";
5
+ type?: string;
6
+ callKey?: string;
7
+ data: unknown;
8
+ }
9
+ export interface RunSummary {
10
+ runId: string;
11
+ question?: string;
12
+ status?: string;
13
+ }
14
+ export interface RunStore {
15
+ append(runId: string, entries: JournalEntry[]): Promise<void>;
16
+ read(runId: string): AsyncIterable<JournalEntry>;
17
+ list(): AsyncIterable<RunSummary>;
18
+ }
19
+ export declare function memoryStore(): RunStore;
20
+ export declare function fileStore(dir: string): RunStore;
21
+ export declare class JournalWriter {
22
+ private readonly store;
23
+ private readonly runId;
24
+ private seq;
25
+ private pending;
26
+ private flushing;
27
+ constructor(store: RunStore, runId: string);
28
+ meta(data: unknown): void;
29
+ event(type: string, data: unknown): void;
30
+ call(callKey: string, data: unknown): void;
31
+ io(callKey: string, data: unknown): void;
32
+ trace(subKind: string, data: unknown): void;
33
+ private push;
34
+ private scheduleFlush;
35
+ flush(): Promise<void>;
36
+ }
37
+ export declare class ReplayCache {
38
+ private readonly byKey;
39
+ private hits;
40
+ add(callKey: string, data: unknown): void;
41
+ take(callKey: string): unknown | undefined;
42
+ values(prefix: string): unknown[];
43
+ get replayedCalls(): number;
44
+ get size(): number;
45
+ }
46
+ export declare function loadReplayCache(store: RunStore, runId: string): Promise<ReplayCache>;
47
+ export declare function loadTrace(store: RunStore, runId: string): Promise<RunTrace | null>;
48
+ export declare function loadRunMeta(store: RunStore, runId: string): Promise<Record<string, unknown> | null>;
@@ -0,0 +1,217 @@
1
+ import { appendFile, mkdir, readdir, readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { TRACE_SCHEMA_VERSION, } from "../trace.js";
4
+ export function memoryStore() {
5
+ const runs = new Map();
6
+ return {
7
+ async append(runId, entries) {
8
+ const existing = runs.get(runId) ?? [];
9
+ existing.push(...entries);
10
+ runs.set(runId, existing);
11
+ },
12
+ async *read(runId) {
13
+ for (const entry of runs.get(runId) ?? [])
14
+ yield entry;
15
+ },
16
+ async *list() {
17
+ for (const [runId, entries] of runs) {
18
+ yield summarize(runId, entries);
19
+ }
20
+ },
21
+ };
22
+ }
23
+ export function fileStore(dir) {
24
+ const fileFor = (runId) => join(dir, `${runId.replace(/[^\w.-]/g, "_")}.jsonl`);
25
+ return {
26
+ async append(runId, entries) {
27
+ if (entries.length === 0)
28
+ return;
29
+ await mkdir(dir, { recursive: true });
30
+ const lines = entries.map((entry) => JSON.stringify(entry)).join("\n");
31
+ await appendFile(fileFor(runId), `${lines}\n`, "utf8");
32
+ },
33
+ async *read(runId) {
34
+ let raw;
35
+ try {
36
+ raw = await readFile(fileFor(runId), "utf8");
37
+ }
38
+ catch {
39
+ return;
40
+ }
41
+ for (const line of raw.split("\n")) {
42
+ const trimmed = line.trim();
43
+ if (!trimmed)
44
+ continue;
45
+ try {
46
+ yield JSON.parse(trimmed);
47
+ }
48
+ catch { }
49
+ }
50
+ },
51
+ async *list() {
52
+ let names;
53
+ try {
54
+ names = await readdir(dir);
55
+ }
56
+ catch {
57
+ return;
58
+ }
59
+ for (const name of names) {
60
+ if (!name.endsWith(".jsonl"))
61
+ continue;
62
+ yield { runId: name.slice(0, -".jsonl".length) };
63
+ }
64
+ },
65
+ };
66
+ }
67
+ function summarize(runId, entries) {
68
+ const meta = entries.find((entry) => entry.kind === "meta");
69
+ const metaData = (meta?.data ?? {});
70
+ const last = [...entries]
71
+ .reverse()
72
+ .find((entry) => entry.kind === "event" &&
73
+ (entry.type === "run.completed" || entry.type === "run.error"));
74
+ return {
75
+ runId,
76
+ ...(metaData.question ? { question: metaData.question } : {}),
77
+ status: last
78
+ ? last.type === "run.completed"
79
+ ? "completed"
80
+ : "failed"
81
+ : "incomplete",
82
+ };
83
+ }
84
+ export class JournalWriter {
85
+ store;
86
+ runId;
87
+ seq = 0;
88
+ pending = [];
89
+ flushing = Promise.resolve();
90
+ constructor(store, runId) {
91
+ this.store = store;
92
+ this.runId = runId;
93
+ }
94
+ meta(data) {
95
+ this.push({ seq: this.seq++, kind: "meta", data });
96
+ }
97
+ event(type, data) {
98
+ this.push({ seq: this.seq++, kind: "event", type, data });
99
+ }
100
+ call(callKey, data) {
101
+ this.push({ seq: this.seq++, kind: "call", callKey, data });
102
+ }
103
+ io(callKey, data) {
104
+ this.push({ seq: this.seq++, kind: "io", callKey, data });
105
+ }
106
+ trace(subKind, data) {
107
+ this.push({ seq: this.seq++, kind: "trace", type: subKind, data });
108
+ }
109
+ push(entry) {
110
+ this.pending.push(entry);
111
+ this.scheduleFlush();
112
+ }
113
+ scheduleFlush() {
114
+ this.flushing = this.flushing.then(async () => {
115
+ if (this.pending.length === 0)
116
+ return;
117
+ const batch = this.pending;
118
+ this.pending = [];
119
+ try {
120
+ await this.store.append(this.runId, batch);
121
+ }
122
+ catch {
123
+ return;
124
+ }
125
+ });
126
+ }
127
+ async flush() {
128
+ await this.flushing;
129
+ if (this.pending.length > 0) {
130
+ const batch = this.pending;
131
+ this.pending = [];
132
+ try {
133
+ await this.store.append(this.runId, batch);
134
+ }
135
+ catch {
136
+ return;
137
+ }
138
+ }
139
+ }
140
+ }
141
+ export class ReplayCache {
142
+ byKey = new Map();
143
+ hits = 0;
144
+ add(callKey, data) {
145
+ const queue = this.byKey.get(callKey) ?? [];
146
+ queue.push(data);
147
+ this.byKey.set(callKey, queue);
148
+ }
149
+ take(callKey) {
150
+ const queue = this.byKey.get(callKey);
151
+ if (!queue || queue.length === 0)
152
+ return undefined;
153
+ this.hits++;
154
+ return queue.shift();
155
+ }
156
+ values(prefix) {
157
+ const found = [];
158
+ for (const [key, queue] of this.byKey) {
159
+ if (key.startsWith(prefix))
160
+ found.push(...queue);
161
+ }
162
+ return found;
163
+ }
164
+ get replayedCalls() {
165
+ return this.hits;
166
+ }
167
+ get size() {
168
+ let total = 0;
169
+ for (const queue of this.byKey.values())
170
+ total += queue.length;
171
+ return total;
172
+ }
173
+ }
174
+ export async function loadReplayCache(store, runId) {
175
+ const cache = new ReplayCache();
176
+ for await (const entry of store.read(runId)) {
177
+ if ((entry.kind === "call" || entry.kind === "io") && entry.callKey) {
178
+ cache.add(entry.callKey, entry.data);
179
+ }
180
+ }
181
+ return cache;
182
+ }
183
+ export async function loadTrace(store, runId) {
184
+ const spans = [];
185
+ const steps = [];
186
+ let digest;
187
+ let found = false;
188
+ for await (const entry of store.read(runId)) {
189
+ if (entry.kind !== "trace")
190
+ continue;
191
+ found = true;
192
+ if (entry.type === "span")
193
+ spans.push(entry.data);
194
+ else if (entry.type === "step")
195
+ steps.push(entry.data);
196
+ else if (entry.type === "digest")
197
+ digest = entry.data;
198
+ }
199
+ if (!found)
200
+ return null;
201
+ return {
202
+ schemaVersion: TRACE_SCHEMA_VERSION,
203
+ mode: steps.length > 0 ? "full" : "spans",
204
+ spans,
205
+ steps,
206
+ ...(digest ? { digest } : {}),
207
+ degraded: Boolean(digest?.degraded),
208
+ };
209
+ }
210
+ export async function loadRunMeta(store, runId) {
211
+ for await (const entry of store.read(runId)) {
212
+ if (entry.kind === "meta") {
213
+ return (entry.data ?? {});
214
+ }
215
+ }
216
+ return null;
217
+ }