@zenera/server 0.0.0-stage → 1.2.1

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/dist/memory.js ADDED
@@ -0,0 +1,471 @@
1
+ // Read-only inspection of a chat's memory graph. Every operation is rooted at the
2
+ // ChatStore's memoryDir for the thread (sessions.ts); no route accepts or derives a
3
+ // filesystem path from input.
4
+ // memoryRouter(store) - the routes
5
+ import { KeyStore } from '@zenera/engine';
6
+ import { isMemory, MemoryError, MemoryIndex, MemoryStore, buildMemoryReport, buildMemorySlice, grepMemory, projectRegistry, readProjectConfig, renderMemoryHtml, renderRecollection, } from '@zenera/neo';
7
+ import { Router } from 'express';
8
+ import { settings } from "./config.js";
9
+ import { HttpError } from "./http.js";
10
+ import { applyProjectEnv } from "./projectEnv.js";
11
+ import { asHttpError, errorMessage, usesMemory } from "./store.js";
12
+ const EMPTY_STATS = { nodes: 0, edges: 0, superseded: 0, files: 0 };
13
+ const MAX_LIMIT = 100;
14
+ const DEFAULT_LIMIT = 30;
15
+ const MAX_OFFSET = 10_000;
16
+ const MAX_KIND_LENGTH = 64;
17
+ const MAX_QUERY_LENGTH = 200;
18
+ const MAX_RECALL_QUERY_LENGTH = 500;
19
+ const MAX_AUDIENCE_LENGTH = 64;
20
+ const GREP_FIELDS = new Set(['text', 'metadata', 'file']);
21
+ function translateMemoryError(err) {
22
+ if (err instanceof HttpError)
23
+ throw err;
24
+ if (err instanceof MemoryError)
25
+ throw new HttpError(422, err.message);
26
+ const mapped = asHttpError(err);
27
+ if (mapped !== err)
28
+ throw mapped;
29
+ throw new HttpError(500, `could not read memory: ${errorMessage(err)}`);
30
+ }
31
+ async function openMemory(dir) {
32
+ let enabled;
33
+ try {
34
+ enabled = usesMemory();
35
+ }
36
+ catch (err) {
37
+ translateMemoryError(err);
38
+ }
39
+ if (!enabled || !dir)
40
+ return { status: 'disabled' };
41
+ if (!isMemory(dir))
42
+ return { status: 'not-created' };
43
+ try {
44
+ return {
45
+ status: 'ready',
46
+ store: await MemoryStore.open(dir, { readOnly: true }),
47
+ };
48
+ }
49
+ catch (err) {
50
+ translateMemoryError(err);
51
+ }
52
+ }
53
+ function one(req, key) {
54
+ const value = req.query[key];
55
+ if (value === undefined)
56
+ return undefined;
57
+ if (typeof value !== 'string')
58
+ throw new HttpError(400, `${key} must be specified once`);
59
+ return value;
60
+ }
61
+ function assertQueryKeys(req, allowed) {
62
+ const accepted = new Set(allowed);
63
+ for (const key of Object.keys(req.query)) {
64
+ if (!accepted.has(key))
65
+ throw new HttpError(400, `unsupported query parameter: ${key}`);
66
+ }
67
+ }
68
+ function booleanParam(req, key, fallback = false) {
69
+ const value = one(req, key);
70
+ if (value === undefined)
71
+ return fallback;
72
+ if (value === 'true')
73
+ return true;
74
+ if (value === 'false')
75
+ return false;
76
+ throw new HttpError(400, `${key} must be true or false`);
77
+ }
78
+ function integerParam(req, key, fallback, minimum, maximum) {
79
+ const value = one(req, key);
80
+ if (value === undefined)
81
+ return fallback;
82
+ if (!/^(0|[1-9]\d*)$/.test(value))
83
+ throw new HttpError(400, `${key} must be an integer`);
84
+ const parsed = Number(value);
85
+ if (!Number.isSafeInteger(parsed) || parsed < minimum || parsed > maximum) {
86
+ throw new HttpError(400, `${key} must be between ${minimum} and ${maximum}`);
87
+ }
88
+ return parsed;
89
+ }
90
+ function listParam(req, key) {
91
+ const value = one(req, key);
92
+ if (value === undefined)
93
+ return undefined;
94
+ if (value.length > 256)
95
+ throw new HttpError(400, `${key} must be 256 characters or fewer`);
96
+ const values = value.split(',');
97
+ if (values.length > 20 ||
98
+ values.some((part) => !part || part.length > MAX_KIND_LENGTH) ||
99
+ new Set(values).size !== values.length) {
100
+ throw new HttpError(400, `${key} contains an invalid or repeated value`);
101
+ }
102
+ return values;
103
+ }
104
+ function checkKind(kind) {
105
+ if (!kind || kind.length > MAX_KIND_LENGTH) {
106
+ throw new HttpError(400, `kind must be 1-${MAX_KIND_LENGTH} characters`);
107
+ }
108
+ return kind;
109
+ }
110
+ function emptyPage(status, offset, limit) {
111
+ return {
112
+ status,
113
+ nodes: [],
114
+ found: 0,
115
+ offset,
116
+ limit,
117
+ truncated: false,
118
+ kinds: [],
119
+ audiences: [],
120
+ };
121
+ }
122
+ function emptyGrep(status) {
123
+ return { status, found: 0, matches: [], truncated: false, skipped: [] };
124
+ }
125
+ function overviewFromReport(report) {
126
+ return {
127
+ status: 'ready',
128
+ stats: {
129
+ nodes: report.nodes.length,
130
+ edges: report.edges.length,
131
+ superseded: report.nodes.filter((node) => node.stale).length,
132
+ files: report.nodes.filter((node) => node.file !== undefined).length,
133
+ ...(report.embedding ? { embedding: report.embedding } : {}),
134
+ ...(report.vectors ? { vectors: report.vectors } : {}),
135
+ },
136
+ kinds: report.kinds,
137
+ audiences: report.audiences,
138
+ };
139
+ }
140
+ const recalling = new Set();
141
+ let embedderOverride;
142
+ /** Test-only seam: replaces credential and model resolution for recall. */
143
+ export function setRecallEmbedderFactory(factory) {
144
+ embedderOverride = factory;
145
+ }
146
+ function numberParam(req, key, minimum, maximum) {
147
+ const value = one(req, key);
148
+ if (value === undefined)
149
+ return undefined;
150
+ if (!/^(\d+(\.\d+)?|\.\d+)$/.test(value))
151
+ throw new HttpError(400, `${key} must be a number`);
152
+ const parsed = Number(value);
153
+ if (!Number.isFinite(parsed) || parsed < minimum || parsed > maximum) {
154
+ throw new HttpError(400, `${key} must be between ${minimum} and ${maximum}`);
155
+ }
156
+ return parsed;
157
+ }
158
+ function projectConfig() {
159
+ try {
160
+ return readProjectConfig(settings().projectDir).config;
161
+ }
162
+ catch (err) {
163
+ throw new HttpError(422, errorMessage(err));
164
+ }
165
+ }
166
+ // Mirrors how a run resolves its memory embedder (openEngine + load.ts): the
167
+ // project's env file, then the keyring, then memory.embedding ?? embedding through
168
+ // the project's model registry.
169
+ async function chooseEmbedder(store, config) {
170
+ if (!store.embedding)
171
+ return { reason: 'this memory holds no vectors' };
172
+ try {
173
+ if (embedderOverride) {
174
+ const embedder = embedderOverride(config);
175
+ return embedder ? { embedder } : { reason: 'the project declares no embedding model' };
176
+ }
177
+ applyProjectEnv();
178
+ (await KeyStore.open()).materialize();
179
+ const ref = config.memory?.embedding ?? config.embedding;
180
+ if (!ref)
181
+ return { reason: 'the project declares no embedding model' };
182
+ const resolved = config.embeddings?.[ref] ?? ref;
183
+ // The config keeps OpenRouter's quantizations as plain strings; the registry
184
+ // checks them, as neo's own loader does.
185
+ return { embedder: projectRegistry(config).embedder(resolved) };
186
+ }
187
+ catch (err) {
188
+ return { reason: `the embedding model could not be built: ${errorMessage(err)}` };
189
+ }
190
+ }
191
+ function emptyRecall(status, query) {
192
+ return {
193
+ status,
194
+ ranking: { by: 'term overlap' },
195
+ sees: [],
196
+ query,
197
+ seeds: [],
198
+ truncated: false,
199
+ nodes: [],
200
+ edges: [],
201
+ block: '',
202
+ };
203
+ }
204
+ export function memoryRouter(chats) {
205
+ const memoryRouter = Router();
206
+ memoryRouter.get('/:tid/memory', async (req, res) => {
207
+ const t = chats.thread(req);
208
+ assertQueryKeys(req, []);
209
+ const opened = await openMemory(chats.memoryDir(t));
210
+ if (opened.status !== 'ready') {
211
+ res.json({
212
+ status: opened.status,
213
+ stats: EMPTY_STATS,
214
+ kinds: [],
215
+ audiences: [],
216
+ });
217
+ return;
218
+ }
219
+ try {
220
+ res.json(overviewFromReport(await buildMemoryReport(opened.store, { maxContentBytes: 0 })));
221
+ }
222
+ catch (err) {
223
+ translateMemoryError(err);
224
+ }
225
+ });
226
+ memoryRouter.get('/:tid/memory/nodes', async (req, res) => {
227
+ const t = chats.thread(req);
228
+ assertQueryKeys(req, ['offset', 'limit', 'kind', 'includeSuperseded', 'all']);
229
+ const offset = integerParam(req, 'offset', 0, 0, MAX_OFFSET);
230
+ const limit = integerParam(req, 'limit', DEFAULT_LIMIT, 1, MAX_LIMIT);
231
+ const all = booleanParam(req, 'all');
232
+ const includeSuperseded = booleanParam(req, 'includeSuperseded') || all;
233
+ const kind = one(req, 'kind');
234
+ if (kind !== undefined)
235
+ checkKind(kind);
236
+ const opened = await openMemory(chats.memoryDir(t));
237
+ if (opened.status !== 'ready') {
238
+ res.json(emptyPage(opened.status, offset, limit));
239
+ return;
240
+ }
241
+ try {
242
+ const graphNodes = opened.store.graph.nodes();
243
+ if (kind && !graphNodes.some((node) => node.kind === kind)) {
244
+ throw new HttpError(400, `unknown memory kind: ${kind}`);
245
+ }
246
+ const stale = opened.store.graph.superseded();
247
+ const matching = graphNodes
248
+ .filter((node) => (!kind || node.kind === kind) && (includeSuperseded || !stale.has(node.id)))
249
+ .sort((a, b) => b.createdAt.localeCompare(a.createdAt) || a.id.localeCompare(b.id));
250
+ const pageIds = matching.slice(offset, offset + limit).map((node) => node.id);
251
+ const slice = await buildMemorySlice(opened.store, pageIds);
252
+ const byId = new Map(slice.nodes.map((node) => [node.id, node]));
253
+ const kinds = [...new Set(graphNodes.map((node) => node.kind))].sort();
254
+ const audiences = [...new Set(graphNodes.flatMap((node) => node.audience))].sort();
255
+ res.json({
256
+ status: 'ready',
257
+ nodes: pageIds.flatMap((nodeId) => {
258
+ const node = byId.get(nodeId);
259
+ return node ? [node] : [];
260
+ }),
261
+ found: matching.length,
262
+ offset,
263
+ limit,
264
+ truncated: offset + limit < matching.length,
265
+ kinds,
266
+ audiences,
267
+ });
268
+ }
269
+ catch (err) {
270
+ translateMemoryError(err);
271
+ }
272
+ });
273
+ memoryRouter.get('/:tid/memory/nodes/:nodeId', async (req, res) => {
274
+ const t = chats.thread(req);
275
+ assertQueryKeys(req, []);
276
+ const nodeId = req.params.nodeId;
277
+ if (!nodeId || nodeId.length > 200)
278
+ throw new HttpError(400, 'nodeId must be 1-200 characters');
279
+ const opened = await openMemory(chats.memoryDir(t));
280
+ if (opened.status !== 'ready') {
281
+ throw new HttpError(404, opened.status === 'disabled' ? 'memory is disabled' : 'memory not created');
282
+ }
283
+ try {
284
+ if (!opened.store.graph.has(nodeId))
285
+ throw new HttpError(404, 'memory node not found');
286
+ const slice = await buildMemorySlice(opened.store, [nodeId]);
287
+ const node = slice.nodes[0];
288
+ if (!node)
289
+ throw new HttpError(404, 'memory node not found');
290
+ res.json({
291
+ status: 'ready',
292
+ node,
293
+ edges: opened.store.graph.neighbors(nodeId),
294
+ });
295
+ }
296
+ catch (err) {
297
+ translateMemoryError(err);
298
+ }
299
+ });
300
+ memoryRouter.get('/:tid/memory/grep', async (req, res) => {
301
+ const t = chats.thread(req);
302
+ if (one(req, 'regex') !== undefined) {
303
+ throw new HttpError(400, 'regex searches are not supported');
304
+ }
305
+ assertQueryKeys(req, ['query', 'in', 'kind', 'caseSensitive', 'all', 'limit']);
306
+ const query = one(req, 'query') ?? '';
307
+ if (!query.trim() || query.length > MAX_QUERY_LENGTH) {
308
+ throw new HttpError(400, `query must be 1-${MAX_QUERY_LENGTH} characters`);
309
+ }
310
+ const fields = listParam(req, 'in');
311
+ if (fields?.some((field) => !GREP_FIELDS.has(field))) {
312
+ throw new HttpError(400, 'in must contain only text, metadata, or file');
313
+ }
314
+ const kinds = listParam(req, 'kind')?.map(checkKind);
315
+ const caseSensitive = booleanParam(req, 'caseSensitive');
316
+ const all = booleanParam(req, 'all');
317
+ const limit = integerParam(req, 'limit', DEFAULT_LIMIT, 1, MAX_LIMIT);
318
+ const opened = await openMemory(chats.memoryDir(t));
319
+ if (opened.status !== 'ready') {
320
+ res.json(emptyGrep(opened.status));
321
+ return;
322
+ }
323
+ try {
324
+ const kindsInMemory = new Set(opened.store.graph.nodes().map((node) => node.kind));
325
+ const unknownKind = kinds?.find((kind) => !kindsInMemory.has(kind));
326
+ if (unknownKind)
327
+ throw new HttpError(400, `unknown memory kind: ${unknownKind}`);
328
+ const result = await grepMemory(opened.store, query, {
329
+ in: fields,
330
+ kinds,
331
+ caseSensitive,
332
+ stale: all ? 'include' : 'exclude',
333
+ limit,
334
+ });
335
+ res.json({ status: 'ready', ...result });
336
+ }
337
+ catch (err) {
338
+ translateMemoryError(err);
339
+ }
340
+ });
341
+ memoryRouter.get('/:tid/memory/report', async (req, res) => {
342
+ const t = chats.thread(req);
343
+ assertQueryKeys(req, []);
344
+ const opened = await openMemory(chats.memoryDir(t));
345
+ if (opened.status !== 'ready') {
346
+ throw new HttpError(404, opened.status === 'disabled' ? 'memory is disabled' : 'memory not created');
347
+ }
348
+ try {
349
+ const report = await buildMemoryReport(opened.store);
350
+ // The library includes the host directory in its report; it has no place
351
+ // in HTML delivered to a caller.
352
+ const html = renderMemoryHtml({ ...report, dir: 'user memory' });
353
+ res.json({ html });
354
+ }
355
+ catch (err) {
356
+ translateMemoryError(err);
357
+ }
358
+ });
359
+ memoryRouter.get('/:tid/memory/recall', async (req, res) => {
360
+ const t = chats.thread(req);
361
+ assertQueryKeys(req, [
362
+ 'query',
363
+ 'kind',
364
+ 'audience',
365
+ 'limit',
366
+ 'hops',
367
+ 'nodes',
368
+ 'minScore',
369
+ 'all',
370
+ ]);
371
+ const text = (one(req, 'query') ?? '').trim();
372
+ if (!text || text.length > MAX_RECALL_QUERY_LENGTH) {
373
+ throw new HttpError(400, `query must be 1-${MAX_RECALL_QUERY_LENGTH} characters`);
374
+ }
375
+ const kind = one(req, 'kind');
376
+ if (kind !== undefined)
377
+ checkKind(kind);
378
+ const audience = one(req, 'audience');
379
+ if (audience !== undefined && (!audience || audience.length > MAX_AUDIENCE_LENGTH)) {
380
+ throw new HttpError(400, `audience must be 1-${MAX_AUDIENCE_LENGTH} characters`);
381
+ }
382
+ const limit = integerParam(req, 'limit', 10, 1, 25);
383
+ const hops = integerParam(req, 'hops', 2, 0, 4);
384
+ const maxNodes = integerParam(req, 'nodes', 25, 1, 50);
385
+ const minScore = numberParam(req, 'minScore', 0, 1);
386
+ const all = booleanParam(req, 'all');
387
+ const echo = {
388
+ text,
389
+ ...(kind !== undefined ? { kind } : {}),
390
+ limit,
391
+ hops,
392
+ nodes: maxNodes,
393
+ ...(minScore !== undefined ? { minScore } : {}),
394
+ };
395
+ res.setHeader('Cache-Control', 'no-store');
396
+ if (recalling.has(t.owner))
397
+ throw new HttpError(409, 'a recall is already running');
398
+ recalling.add(t.owner);
399
+ try {
400
+ const opened = await openMemory(chats.memoryDir(t));
401
+ if (opened.status !== 'ready') {
402
+ res.json(emptyRecall(opened.status, echo));
403
+ return;
404
+ }
405
+ const { store } = opened;
406
+ const graphNodes = store.graph.nodes();
407
+ if (kind && !graphNodes.some((node) => node.kind === kind)) {
408
+ throw new HttpError(400, `unknown memory kind: ${kind}`);
409
+ }
410
+ const sees = audience === undefined
411
+ ? [...new Set([...graphNodes.flatMap((node) => node.audience), '*'])].sort()
412
+ : [...new Set([audience, '*'])];
413
+ const config = projectConfig();
414
+ const choice = await chooseEmbedder(store, config);
415
+ const embedder = 'embedder' in choice ? choice.embedder : undefined;
416
+ const ranking = embedder
417
+ ? { by: 'meaning', model: embedder.id }
418
+ : { by: 'term overlap', reason: choice.reason };
419
+ const index = new MemoryIndex({
420
+ store,
421
+ embedder,
422
+ kinds: config.memory?.kinds,
423
+ relations: config.memory?.relations,
424
+ });
425
+ const memoryQuery = {
426
+ text,
427
+ ...(kind !== undefined ? { kinds: [kind] } : {}),
428
+ limit,
429
+ maxHops: hops,
430
+ maxNodes,
431
+ ...(minScore !== undefined ? { minScore } : {}),
432
+ };
433
+ let rec;
434
+ try {
435
+ rec = await index.search(memoryQuery, sees, { stale: all });
436
+ }
437
+ catch (err) {
438
+ if (embedder && !(err instanceof MemoryError) && !(err instanceof HttpError)) {
439
+ throw new HttpError(502, `embedding the query failed: ${errorMessage(err)}`);
440
+ }
441
+ throw err;
442
+ }
443
+ const stale = store.graph.superseded(sees);
444
+ res.json({
445
+ status: 'ready',
446
+ ranking,
447
+ sees,
448
+ query: echo,
449
+ seeds: rec.seeds,
450
+ truncated: rec.truncated,
451
+ nodes: rec.nodes.map((scored) => ({
452
+ node: scored.node,
453
+ score: scored.score,
454
+ seed: scored.seed,
455
+ ...(scored.via ? { via: scored.via } : {}),
456
+ stale: stale.has(scored.node.id),
457
+ })),
458
+ edges: rec.edges,
459
+ block: renderRecollection(rec, { tagged: false }),
460
+ });
461
+ }
462
+ catch (err) {
463
+ translateMemoryError(err);
464
+ }
465
+ finally {
466
+ recalling.delete(t.owner);
467
+ }
468
+ });
469
+ return memoryRouter;
470
+ }
471
+ //# sourceMappingURL=memory.js.map
@@ -0,0 +1,5 @@
1
+ import type { ZenPlugin } from '@zenera/engine';
2
+ export declare function envRunPlugins(env?: NodeJS.ProcessEnv): (run: {
3
+ mcpToken: string | null;
4
+ }) => Promise<ZenPlugin[]>;
5
+ //# sourceMappingURL=plugins.d.ts.map
@@ -0,0 +1,29 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { pathToFileURL } from 'node:url';
10
+ export function envRunPlugins(env = process.env) {
11
+ const plugin = (env.ZEN_PLUGINS ?? '')
12
+ .split(',')
13
+ .map((p) => p.trim())
14
+ .find((p) => p.endsWith('/mcp.mjs')) ?? '';
15
+ const brokerUrl = env.ZEN_MCP_BROKER_URL ?? '';
16
+ let loaded = null;
17
+ return async ({ mcpToken }) => {
18
+ if (!mcpToken || !plugin || !brokerUrl)
19
+ return [];
20
+ loaded ??= import(__rewriteRelativeImportExtension(pathToFileURL(plugin).href)).catch((err) => {
21
+ console.warn(`mcp plugin ${plugin}: ${err instanceof Error ? err.message : err}`);
22
+ loaded = null;
23
+ return {};
24
+ });
25
+ const { mcpPlugin } = await loaded;
26
+ return mcpPlugin ? [mcpPlugin({ brokerUrl, token: mcpToken })] : [];
27
+ };
28
+ }
29
+ //# sourceMappingURL=plugins.js.map
@@ -0,0 +1,3 @@
1
+ export declare function applyProjectEnv(): void;
2
+ export declare function withoutProjectEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
3
+ //# sourceMappingURL=projectEnv.d.ts.map
@@ -0,0 +1,37 @@
1
+ // The project's env file, applied to this process before a chat turn opens its engine. Which file
2
+ // is the engine's choice: $ZEN_ENV_FILE when set (the manager sets .env.test, for test chats),
3
+ // .env otherwise (the runtime, for private chats).
4
+ // The engine only fills names that are not set yet, which in a long-lived process means the
5
+ // first value it read stays until the container restarts. Applying the file here first, and
6
+ // overwriting, makes an edit reach the next turn's sandbox: changed values replace the old ones,
7
+ // and names no longer in the file are removed. The pod's own variables (BIFROST_*, ZEN_*, HOME,
8
+ // PATH...) are never touched, so a project file cannot redirect the pod.
9
+ // applyProjectEnv - set what the file says, drop what it stopped saying
10
+ // withoutProjectEnv - an environment for a zen subprocess: none of that, and no ZEN_ENV_FILE,
11
+ // so a zen command run from the editor reads .env like it would anywhere
12
+ import { ENV_FILE_ENV, readProjectEnv } from '@zenera/engine';
13
+ import { settings } from "./config.js";
14
+ const POD_ENV = new Set(Object.keys(process.env));
15
+ let applied = new Set();
16
+ export function applyProjectEnv() {
17
+ const values = readProjectEnv(settings().projectDir)?.values ?? {};
18
+ for (const name of applied) {
19
+ if (!(name in values))
20
+ delete process.env[name];
21
+ }
22
+ applied = new Set();
23
+ for (const [name, value] of Object.entries(values)) {
24
+ if (POD_ENV.has(name))
25
+ continue;
26
+ process.env[name] = value;
27
+ applied.add(name);
28
+ }
29
+ }
30
+ export function withoutProjectEnv(env) {
31
+ const out = { ...env };
32
+ for (const name of applied)
33
+ delete out[name];
34
+ delete out[ENV_FILE_ENV];
35
+ return out;
36
+ }
37
+ //# sourceMappingURL=projectEnv.js.map
@@ -0,0 +1,27 @@
1
+ import { type AGUIEvent } from '@ag-ui/core';
2
+ export declare function compact(events: AGUIEvent[]): AGUIEvent[];
3
+ export declare function isEnd(e: AGUIEvent | undefined): boolean;
4
+ export interface Follower {
5
+ events(batch: AGUIEvent[]): void;
6
+ end(): void;
7
+ }
8
+ export declare class RunLog {
9
+ #private;
10
+ readonly runId: string;
11
+ readonly file: string;
12
+ private constructor();
13
+ static open(dir: string, runId: string, maxBytes: number): RunLog;
14
+ get ended(): boolean;
15
+ append(events: AGUIEvent[]): void;
16
+ /** Everything so far at once, then each batch as it comes; returns the unfollow. */
17
+ follow(f: Follower): () => void;
18
+ /** Lets the followers go, then rewrites the file compacted. */
19
+ end(): Promise<void>;
20
+ }
21
+ export declare function threadRunLogs(dir: string): Promise<{
22
+ runId: string;
23
+ file: string;
24
+ }[]>;
25
+ /** Only for a run no longer going: one without an end was cut off by a restart. */
26
+ export declare function readRunLog(file: string): Promise<AGUIEvent[]>;
27
+ //# sourceMappingURL=runLog.d.ts.map