@skein-js/server-kit 0.11.3 → 0.12.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/dist/index.js CHANGED
@@ -1,225 +1,139 @@
1
- // src/resolve-runtime.ts
2
1
  import {
3
- createProtocolRuntime
4
- } from "@skein-js/agent-protocol";
2
+ corsFromHttpConfig,
3
+ createInMemoryDeps,
4
+ disabledRoutesFromHttpConfig,
5
+ embedInMemoryGraphs,
6
+ graphMapToResolver,
7
+ loadInMemoryRuntime,
8
+ loadReloadableInMemoryRuntime,
9
+ normalizeEmbeddableGraphs,
10
+ positiveIntegerFromEnv,
11
+ requirePositiveInteger,
12
+ resolveMaxPageSize,
13
+ resolveMemoryBusLimits,
14
+ routesFromHttpConfig,
15
+ toCorsOptions
16
+ } from "./chunk-QOHYARHD.js";
17
+ import "./chunk-JUBBCC7R.js";
5
18
 
6
- // src/in-memory-runtime.ts
7
- import { MemorySaver as MemorySaver2 } from "@langchain/langgraph";
19
+ // src/resolve-runtime.ts
8
20
  import {
9
- loadAuthEngine,
10
- loadConfig
11
- } from "@skein-js/config";
12
- import { MemoryRunEventBus as MemoryRunEventBus2, MemoryRunQueue as MemoryRunQueue2, MemorySkeinStore as MemorySkeinStore2 } from "@skein-js/storage-memory";
21
+ createProtocolRuntime,
22
+ skeinRoutes
23
+ } from "@skein-js/agent-protocol";
13
24
 
14
- // src/cors-config.ts
15
- var ALWAYS_EXPOSED_HEADERS = ["content-location", "x-pagination-total"];
16
- function toCorsOptions(config) {
17
- const options = {};
18
- const allowAll = config.allow_origins?.includes("*") ?? false;
19
- if (config.allow_origin_regex !== void 0) {
20
- const pattern = new RegExp(`^(?:${config.allow_origin_regex})$`);
21
- const listed = allowAll ? void 0 : new Set(config.allow_origins);
22
- options.origin = (origin, callback) => callback(
23
- null,
24
- origin !== void 0 && (allowAll || pattern.test(origin) || (listed?.has(origin) ?? false))
25
- );
26
- } else if (config.allow_origins !== void 0) {
27
- options.origin = allowAll ? "*" : config.allow_origins;
28
- }
29
- if (config.allow_methods !== void 0) options.methods = config.allow_methods;
30
- if (config.allow_headers !== void 0) options.allowedHeaders = config.allow_headers;
31
- if (config.allow_credentials !== void 0) options.credentials = config.allow_credentials;
32
- if (config.max_age !== void 0) options.maxAge = config.max_age;
33
- const exposed = /* @__PURE__ */ new Set([...ALWAYS_EXPOSED_HEADERS, ...config.expose_headers ?? []]);
34
- options.exposedHeaders = [...exposed];
35
- return options;
36
- }
37
- function corsFromHttpConfig(http) {
38
- if (typeof http !== "object" || http === null) return void 0;
39
- const cors = http.cors;
40
- if (typeof cors !== "object" || cors === null) return void 0;
41
- return toCorsOptions(cors);
25
+ // src/cron-tick.ts
26
+ import { DEFAULT_CRON_TICK_MS } from "@skein-js/agent-protocol";
27
+ var CRON_TICK_ENV_VARS = ["SKEIN_CRON_TICK_MS"];
28
+ function resolveCronTickMs(explicit, env = process.env) {
29
+ const fromEnv = positiveIntegerFromEnv(CRON_TICK_ENV_VARS, env);
30
+ if (explicit === void 0) return fromEnv ?? DEFAULT_CRON_TICK_MS;
31
+ return requirePositiveInteger("scheduler.tickMs", explicit);
42
32
  }
43
33
 
44
- // src/dev-persistence.ts
45
- var toBase64 = (bytes) => Buffer.from(bytes).toString("base64");
46
- var fromBase64 = (text) => new Uint8Array(Buffer.from(text, "base64"));
47
- var mapValues = (record, fn) => Object.fromEntries(Object.entries(record).map(([key, value]) => [key, fn(value)]));
48
- function snapshotCheckpointer(saver) {
49
- return {
50
- storage: mapValues(
51
- saver.storage,
52
- (namespaces) => mapValues(
53
- namespaces,
54
- (checkpoints) => mapValues(checkpoints, ([checkpoint, metadata, parentId]) => [
55
- toBase64(checkpoint),
56
- toBase64(metadata),
57
- parentId
58
- ])
59
- )
60
- ),
61
- writes: mapValues(
62
- saver.writes,
63
- (taskWrites) => mapValues(taskWrites, ([taskId, channel, blob]) => [
64
- taskId,
65
- channel,
66
- toBase64(blob)
67
- ])
68
- )
69
- };
70
- }
71
- function hydrateCheckpointer(saver, snapshot) {
72
- saver.storage = mapValues(
73
- snapshot.storage,
74
- (namespaces) => mapValues(
75
- namespaces,
76
- (checkpoints) => mapValues(
77
- checkpoints,
78
- ([checkpoint, metadata, parentId]) => [
79
- fromBase64(checkpoint),
80
- fromBase64(metadata),
81
- parentId
82
- ]
83
- )
84
- )
85
- );
86
- saver.writes = mapValues(
87
- snapshot.writes,
88
- (taskWrites) => mapValues(taskWrites, ([taskId, channel, blob]) => [
89
- taskId,
90
- channel,
91
- fromBase64(blob)
92
- ])
93
- );
94
- }
95
-
96
- // src/in-memory-deps.ts
97
- import { MemorySaver } from "@langchain/langgraph";
98
- import { MemoryRunEventBus, MemoryRunQueue, MemorySkeinStore } from "@skein-js/storage-memory";
99
- function isGraphResolver(graphs) {
100
- const candidate = graphs;
101
- return Array.isArray(candidate.ids) && typeof candidate.load === "function";
102
- }
103
- function normalizeEmbeddableGraphs(graphs) {
104
- return isGraphResolver(graphs) ? graphs : graphMapToResolver(graphs);
105
- }
106
- function graphMapToResolver(graphs) {
107
- const ids = Object.keys(graphs);
108
- return {
109
- ids,
110
- load: async (graphId) => {
111
- const graph = graphs[graphId];
112
- if (graph == null) {
113
- const known = ids.join(", ") || "none";
114
- throw new Error(
115
- ids.includes(graphId) ? `Graph "${graphId}" resolved to ${String(graph)} \u2014 check its export/factory (known: ${known}).` : `Unknown graph "${graphId}" (known: ${known}).`
116
- );
117
- }
118
- return graph;
119
- },
120
- schemas: async (graphId) => ({ [graphId]: { graph_id: graphId } })
121
- };
122
- }
123
- function embedInMemoryGraphs(graphs, overrides = {}) {
124
- return {
125
- store: new MemorySkeinStore(),
126
- graphs: normalizeEmbeddableGraphs(graphs),
127
- queue: new MemoryRunQueue(),
128
- bus: new MemoryRunEventBus(),
129
- checkpointer: new MemorySaver(),
130
- ...overrides
34
+ // src/heap-pressure.ts
35
+ import { getHeapStatistics } from "v8";
36
+ import { SkeinConfigError } from "@skein-js/config/errors";
37
+ var DEFAULT_HEAP_WARN_PERCENT = 85;
38
+ var HEAP_REARM_MARGIN_PERCENT = 15;
39
+ var HEAP_REARM_PERCENT = DEFAULT_HEAP_WARN_PERCENT - HEAP_REARM_MARGIN_PERCENT;
40
+ var DEFAULT_HEAP_SAMPLE_MS = 3e4;
41
+ function megabytes(bytes) {
42
+ return `${Math.round(bytes / 1048576)}MB`;
43
+ }
44
+ function startHeapPressureMonitor(options = {}) {
45
+ const {
46
+ logger,
47
+ warnPercent = DEFAULT_HEAP_WARN_PERCENT,
48
+ sampleMs = DEFAULT_HEAP_SAMPLE_MS,
49
+ context,
50
+ heapStatistics = getHeapStatistics,
51
+ rss = () => process.memoryUsage().rss,
52
+ setInterval: schedule = (callback, ms) => setInterval(callback, ms),
53
+ clearInterval: unschedule = (handle2) => clearInterval(handle2)
54
+ } = options;
55
+ if (!logger || warnPercent <= 0 || sampleMs <= 0) return { stop: () => {
56
+ } };
57
+ const rearmPercent = Math.max(0, warnPercent - HEAP_REARM_MARGIN_PERCENT);
58
+ let warned = false;
59
+ const sample = () => {
60
+ const { heap_size_limit: limit, used_heap_size: used } = heapStatistics();
61
+ if (limit <= 0) return;
62
+ const percent = used / limit * 100;
63
+ if (warned) {
64
+ if (percent < rearmPercent) warned = false;
65
+ return;
66
+ }
67
+ if (percent < warnPercent) return;
68
+ warned = true;
69
+ const extra = context?.() ?? {};
70
+ const details = [
71
+ `heap ${megabytes(used)}/${megabytes(limit)} (${Math.round(percent)}%)`,
72
+ `rss ${megabytes(rss())}`,
73
+ extra.inFlightRuns !== void 0 ? `runs in flight ${extra.inFlightRuns}` : void 0,
74
+ extra.bufferedFrames !== void 0 ? `buffered frames ${extra.bufferedFrames}` : void 0
75
+ ].filter(Boolean);
76
+ logger.warn?.(
77
+ `skein: heap pressure \u2014 ${details.join(", ")}. Many runs in flight suggests lowering SKEIN_RUN_CONCURRENCY; a large buffered-frame count suggests a slow SSE consumer (see SKEIN_MEMORY_BUS_MAX_FRAMES_PER_RUN); neither suggests a leak. Next warning only after usage drops below ${rearmPercent}%.`
78
+ );
131
79
  };
132
- }
133
- var createInMemoryDeps = embedInMemoryGraphs;
134
-
135
- // src/in-memory-runtime.ts
136
- function toGraphResolver(graphs) {
80
+ const handle = schedule(sample, sampleMs);
81
+ handle.unref?.();
82
+ let stopped = false;
137
83
  return {
138
- ids: graphs.ids,
139
- load: (graphId) => graphs.load(graphId),
140
- schemas: async (graphId) => await graphs.schemas(graphId)
84
+ stop() {
85
+ if (stopped) return;
86
+ stopped = true;
87
+ unschedule(handle);
88
+ }
141
89
  };
142
90
  }
143
- async function loadInMemoryRuntime(configPath, importModule, staticSchemas) {
144
- const { graphs, config, configDir } = await loadConfig({
145
- configPath,
146
- importModule,
147
- staticSchemas
148
- });
149
- const deps = embedInMemoryGraphs(toGraphResolver(graphs));
150
- deps.auth = await loadAuthEngine(config.auth, { configDir, importModule });
91
+ function resolveHeapPressureOptions(explicit = {}, env = process.env) {
92
+ const warnFromEnv = percentFromEnv(env["SKEIN_HEAP_WARN_PERCENT"]);
93
+ const sampleFromEnv = positiveIntegerFromEnv(["SKEIN_HEAP_SAMPLE_MS"], env);
151
94
  return {
152
- deps,
153
- cors: corsFromHttpConfig(config.http)
95
+ warnPercent: explicit.warnPercent ?? warnFromEnv ?? DEFAULT_HEAP_WARN_PERCENT,
96
+ sampleMs: explicit.sampleMs ?? sampleFromEnv ?? DEFAULT_HEAP_SAMPLE_MS
154
97
  };
155
98
  }
156
- async function loadReloadableInMemoryRuntime(configPath, importModule, staticSchemas, extraDeps) {
157
- const first = await loadConfig({ configPath, importModule, staticSchemas });
158
- let current = first.graphs;
159
- const graphs = {
160
- ids: first.graphs.ids,
161
- load: (graphId) => current.load(graphId),
162
- schemas: async (graphId) => await current.schemas(graphId)
163
- };
164
- const store = new MemorySkeinStore2();
165
- const checkpointer = new MemorySaver2();
166
- const deps = {
167
- store,
168
- graphs,
169
- queue: new MemoryRunQueue2(),
170
- bus: new MemoryRunEventBus2(),
171
- checkpointer,
172
- auth: await loadAuthEngine(first.config.auth, { configDir: first.configDir, importModule }),
173
- ...extraDeps
174
- };
175
- return {
176
- deps,
177
- cors: corsFromHttpConfig(first.config.http),
178
- config: first.config,
179
- configDir: first.configDir,
180
- reloadGraphs: async () => {
181
- current = (await loadConfig({ configPath, importModule, staticSchemas })).graphs;
182
- },
183
- snapshotState: () => ({
184
- version: 1,
185
- store: store.snapshot(),
186
- checkpoints: snapshotCheckpointer(checkpointer)
187
- }),
188
- hydrateState: (snapshot) => {
189
- store.hydrate(snapshot.store);
190
- hydrateCheckpointer(checkpointer, snapshot.checkpoints);
191
- }
192
- };
99
+ function percentFromEnv(raw) {
100
+ if (raw === void 0 || raw.trim() === "") return void 0;
101
+ const value = Number(raw);
102
+ if (!Number.isInteger(value) || value < 0 || value > 100) {
103
+ throw new SkeinConfigError(
104
+ `SKEIN_HEAP_WARN_PERCENT must be an integer between 0 and 100 (got "${raw}").`
105
+ );
106
+ }
107
+ return value;
193
108
  }
194
109
 
195
110
  // src/run-concurrency.ts
196
111
  import { DEFAULT_RUN_CONCURRENCY } from "@skein-js/agent-protocol";
197
- import { SkeinConfigError } from "@skein-js/config";
112
+ var PG_DEFAULT_POOL_MAX = 10;
198
113
  var CONCURRENCY_ENV_VARS = ["SKEIN_RUN_CONCURRENCY", "N_JOBS_PER_WORKER"];
199
- function requirePositiveInteger(source, raw) {
200
- const parsed = Number(raw);
201
- if (!Number.isInteger(parsed) || parsed <= 0) {
202
- throw new SkeinConfigError(`${source} must be a positive integer (got "${String(raw)}").`);
203
- }
204
- return parsed;
205
- }
206
- function runConcurrencyFromEnv(env) {
207
- for (const name of CONCURRENCY_ENV_VARS) {
208
- const raw = env[name];
209
- if (raw === void 0 || raw.trim() === "") continue;
210
- return requirePositiveInteger(name, raw);
211
- }
212
- return void 0;
213
- }
214
114
  function resolveRunConcurrency(explicit, env = process.env) {
215
- const fromEnv = runConcurrencyFromEnv(env);
115
+ const fromEnv = positiveIntegerFromEnv(CONCURRENCY_ENV_VARS, env);
216
116
  if (explicit === void 0) return fromEnv ?? DEFAULT_RUN_CONCURRENCY;
217
117
  return requirePositiveInteger("worker.maxConcurrency", explicit);
218
118
  }
119
+ function describePoolPressure(runConcurrency, poolMax) {
120
+ const effectivePoolMax = poolMax ?? PG_DEFAULT_POOL_MAX;
121
+ if (runConcurrency <= effectivePoolMax) return void 0;
122
+ const source = poolMax === void 0 ? `pg's default of ${PG_DEFAULT_POOL_MAX}` : `PG_POOL_MAX`;
123
+ return `Run concurrency (${runConcurrency}) is higher than the Postgres pool allows (${effectivePoolMax}, from ${source}). Runs will queue waiting for a connection rather than executing, which looks like flat throughput rather than like a pool limit. Raise PG_POOL_MAX to at least ${runConcurrency}, or lower SKEIN_RUN_CONCURRENCY. Note skein opens two pools per instance (store + checkpointer), so budget ${runConcurrency * 2} connections against your database's own cap.`;
124
+ }
125
+
126
+ // src/run-timeout.ts
127
+ var RUN_TIMEOUT_ENV_VARS = ["SKEIN_RUN_TIMEOUT_MS"];
128
+ function resolveRunTimeoutMs(explicit, env = process.env) {
129
+ const fromEnv = positiveIntegerFromEnv(RUN_TIMEOUT_ENV_VARS, env);
130
+ if (explicit === void 0) return fromEnv;
131
+ return requirePositiveInteger("runTimeoutMs", explicit);
132
+ }
219
133
 
220
134
  // src/shutdown-grace.ts
221
135
  import { DEFAULT_SHUTDOWN_GRACE_MS } from "@skein-js/agent-protocol";
222
- import { SkeinConfigError as SkeinConfigError2 } from "@skein-js/config";
136
+ import { SkeinConfigError as SkeinConfigError2 } from "@skein-js/config/errors";
223
137
  var SHUTDOWN_GRACE_ENV_VAR = "SKEIN_SHUTDOWN_GRACE_MS";
224
138
  function requireNonNegativeInteger(source, raw) {
225
139
  const parsed = Number(raw);
@@ -248,18 +162,33 @@ function resolveLogger(option, injected, frameworkLogger) {
248
162
  return option ?? frameworkLogger;
249
163
  }
250
164
  async function resolveRuntimeDeps(options, frameworkLogger) {
251
- const loaded = options.deps ? { deps: options.deps, cors: void 0 } : await loadInMemoryRuntime(options.config, options.importModule);
165
+ const loaded = options.deps ? { deps: options.deps, cors: void 0, routes: skeinRoutes } : await (await import("./in-memory-runtime-32UYJRVJ.js")).loadInMemoryRuntime(options.config, options.importModule);
252
166
  const logger = resolveLogger(options.logger, loaded.deps.logger, frameworkLogger);
253
167
  if (logger && !loaded.deps.logger) loaded.deps.logger = logger;
254
- return { deps: loaded.deps, cors: loaded.cors, logger };
168
+ return { deps: loaded.deps, cors: loaded.cors, routes: loaded.routes, logger };
255
169
  }
256
170
  async function resolveProtocolRuntime(options, frameworkLogger) {
257
- const { deps, cors: corsFromConfig, logger } = await resolveRuntimeDeps(options, frameworkLogger);
171
+ const {
172
+ deps,
173
+ cors: corsFromConfig,
174
+ routes,
175
+ logger
176
+ } = await resolveRuntimeDeps(options, frameworkLogger);
177
+ const heapPressureOptions = resolveHeapPressureOptions();
178
+ const runTimeoutMs = resolveRunTimeoutMs(deps.runTimeoutMs);
179
+ if (runTimeoutMs !== void 0) deps.runTimeoutMs = runTimeoutMs;
258
180
  const runtime = createProtocolRuntime(deps, {
259
181
  worker: {
260
182
  ...options.worker,
261
183
  maxConcurrency: resolveRunConcurrency(options.worker?.maxConcurrency),
262
184
  shutdownGraceMs: resolveShutdownGraceMs(options.worker?.shutdownGraceMs)
185
+ },
186
+ scheduler: {
187
+ ...options.scheduler,
188
+ tickMs: resolveCronTickMs(options.scheduler?.tickMs),
189
+ // `http.disable_crons` takes the whole resource away, so the scheduler goes with it: leaving
190
+ // it ticking would keep firing schedules nobody can see, list, or cancel any more.
191
+ enabled: options.scheduler?.enabled ?? routes.some((route) => route.group === "crons")
263
192
  }
264
193
  });
265
194
  await runtime.service.assistants.registerGraphAssistants();
@@ -273,7 +202,31 @@ async function resolveProtocolRuntime(options, frameworkLogger) {
273
202
  );
274
203
  }
275
204
  runtime.worker.start();
276
- return { runtime, cors: corsFromConfig, logger };
205
+ const heapMonitor = startHeapPressureMonitor({
206
+ ...logger ? { logger } : {},
207
+ ...heapPressureOptions,
208
+ // Read at warn time only. `bufferedFrameCount` is a diagnostic the in-memory bus exposes and the
209
+ // Redis one does not, so it is probed rather than required — on Redis the frames are not in this
210
+ // process's heap anyway, which is itself part of the answer.
211
+ context: () => {
212
+ const bufferedFrames = deps.bus.bufferedFrameCount;
213
+ return {
214
+ inFlightRuns: runtime.worker.inFlightRunCount,
215
+ ...typeof bufferedFrames === "number" ? { bufferedFrames } : {}
216
+ };
217
+ }
218
+ });
219
+ const worker = {
220
+ get inFlightRunCount() {
221
+ return runtime.worker.inFlightRunCount;
222
+ },
223
+ start: () => runtime.worker.start(),
224
+ stop: async () => {
225
+ heapMonitor.stop();
226
+ await runtime.worker.stop();
227
+ }
228
+ };
229
+ return { runtime: { ...runtime, worker }, cors: corsFromConfig, routes, logger };
277
230
  }
278
231
 
279
232
  // src/log-meta.ts
@@ -389,302 +342,65 @@ ${rendered}` : ""}`);
389
342
  };
390
343
  }
391
344
 
392
- // src/index.ts
393
- import { DEFAULT_RUN_CONCURRENCY as DEFAULT_RUN_CONCURRENCY2 } from "@skein-js/agent-protocol";
394
- import { DEFAULT_SHUTDOWN_GRACE_MS as DEFAULT_SHUTDOWN_GRACE_MS2 } from "@skein-js/agent-protocol";
395
-
396
- // src/langgraph-import.ts
397
- import { readFile } from "fs/promises";
398
- import path from "path";
399
- import {
400
- MemorySaver as MemorySaver3
401
- } from "@langchain/langgraph";
402
- import {
403
- isTerminalRunStatus
404
- } from "@skein-js/core";
405
- import { SuperJSON } from "superjson";
406
- import { z } from "zod";
407
- var OPS_FILE = ".langgraphjs_ops.json";
408
- var STORE_FILE = ".langgraphjs_api.store.json";
409
- var CHECKPOINTER_FILE = ".langgraphjs_api.checkpointer.json";
410
- var langgraphSuperjson = new SuperJSON();
411
- langgraphSuperjson.registerCustom(
412
- {
413
- isApplicable: (value) => value instanceof Uint8Array,
414
- serialize: (value) => Buffer.from(value).toString("base64"),
415
- deserialize: (value) => new Uint8Array(Buffer.from(value, "base64"))
416
- },
417
- "Uint8Array"
418
- );
419
- var timestamp = z.union([z.date(), z.string()]).optional();
420
- var jsonObject = z.record(z.unknown());
421
- var langgraphAssistantSchema = z.object({
422
- assistant_id: z.string(),
423
- graph_id: z.string(),
424
- name: z.string().optional(),
425
- description: z.string().nullish(),
426
- config: jsonObject.optional(),
427
- context: z.unknown().optional(),
428
- metadata: jsonObject.optional(),
429
- version: z.number().optional(),
430
- created_at: timestamp,
431
- updated_at: timestamp
432
- }).passthrough();
433
- var langgraphAssistantVersionSchema = z.object({
434
- assistant_id: z.string(),
435
- version: z.number(),
436
- graph_id: z.string(),
437
- name: z.string().optional(),
438
- description: z.string().nullish(),
439
- config: jsonObject.optional(),
440
- context: z.unknown().optional(),
441
- metadata: jsonObject.optional(),
442
- created_at: timestamp
443
- }).passthrough();
444
- var langgraphThreadSchema = z.object({
445
- thread_id: z.string(),
446
- status: z.string().optional(),
447
- metadata: jsonObject.optional(),
448
- values: jsonObject.optional(),
449
- interrupts: jsonObject.optional(),
450
- created_at: timestamp,
451
- updated_at: timestamp
452
- }).passthrough();
453
- var langgraphRunSchema = z.object({
454
- run_id: z.string(),
455
- thread_id: z.string(),
456
- assistant_id: z.string(),
457
- status: z.string().optional(),
458
- metadata: jsonObject.optional(),
459
- multitask_strategy: z.string().nullish(),
460
- kwargs: jsonObject.optional(),
461
- created_at: timestamp,
462
- updated_at: timestamp
463
- }).passthrough();
464
- var langgraphOpsSchema = z.object({
465
- assistants: z.record(langgraphAssistantSchema).optional(),
466
- assistant_versions: z.array(langgraphAssistantVersionSchema).optional(),
467
- threads: z.record(langgraphThreadSchema).optional(),
468
- runs: z.record(langgraphRunSchema).optional()
469
- }).passthrough();
470
- var langgraphStoreItemSchema = z.object({
471
- namespace: z.array(z.string()),
472
- key: z.string(),
473
- value: jsonObject,
474
- createdAt: timestamp,
475
- updatedAt: timestamp
476
- }).passthrough();
477
- var langgraphStoreFileSchema = z.object({ data: z.map(z.string(), z.map(z.string(), langgraphStoreItemSchema)).optional() }).passthrough();
478
- var langgraphCheckpointerSchema = z.object({ storage: jsonObject, writes: jsonObject }).passthrough();
479
- async function readSuperjsonFile(filepath) {
480
- let text;
345
+ // src/heap-headroom.ts
346
+ import { readFileSync } from "fs";
347
+ import { getHeapStatistics as getHeapStatistics2 } from "v8";
348
+ var HEAP_LIMIT_WARN_FRACTION = 0.75;
349
+ var HEAP_LIMIT_SUGGEST_FRACTION = 0.6;
350
+ var MEMORY_LIMIT_FILES = [
351
+ "/sys/fs/cgroup/memory.max",
352
+ "/sys/fs/cgroup/memory/memory.limit_in_bytes"
353
+ ];
354
+ var readFileIfPresent = (path) => {
481
355
  try {
482
- text = await readFile(filepath, "utf8");
356
+ return readFileSync(path, "utf8");
483
357
  } catch {
484
- return null;
485
- }
486
- try {
487
- return langgraphSuperjson.parse(text) ?? null;
488
- } catch (error) {
489
- throw new Error(
490
- `Could not parse LangGraph state file "${filepath}": ${error instanceof Error ? error.message : String(error)}`
491
- );
358
+ return void 0;
492
359
  }
493
- }
494
- function validateShape(schema, value, label) {
495
- const result = schema.safeParse(value);
496
- if (!result.success) {
497
- const detail = result.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
498
- throw new Error(`Invalid LangGraph ${label}: ${detail}`);
360
+ };
361
+ function readContainerMemoryLimit(readFile = readFileIfPresent) {
362
+ for (const path of MEMORY_LIMIT_FILES) {
363
+ const raw = readFile(path)?.trim();
364
+ if (raw === void 0 || raw === "" || raw === "max") continue;
365
+ const bytes = Number(raw);
366
+ if (!Number.isFinite(bytes) || bytes <= 0) continue;
367
+ if (bytes >= Number.MAX_SAFE_INTEGER) continue;
368
+ return bytes;
499
369
  }
500
- return result.data;
501
- }
502
- function toIso(value, fallback) {
503
- if (value instanceof Date) return value.toISOString();
504
- if (typeof value === "string") return value;
505
- return fallback;
506
- }
507
- function toAssistant(row, now) {
508
- return {
509
- assistant_id: row.assistant_id,
510
- graph_id: row.graph_id,
511
- name: row.name ?? row.graph_id,
512
- description: row.description ?? void 0,
513
- config: row.config ?? {},
514
- context: row.context ?? {},
515
- metadata: row.metadata ?? {},
516
- version: row.version ?? 1,
517
- created_at: toIso(row.created_at, now),
518
- updated_at: toIso(row.updated_at, now)
519
- };
520
- }
521
- function toAssistantVersion(row, now) {
522
- return {
523
- assistant_id: row.assistant_id,
524
- version: row.version,
525
- graph_id: row.graph_id,
526
- name: row.name ?? row.graph_id,
527
- description: row.description ?? void 0,
528
- config: row.config ?? {},
529
- context: row.context ?? {},
530
- metadata: row.metadata ?? {},
531
- created_at: toIso(row.created_at, now)
532
- };
533
- }
534
- function toThread(row, now) {
535
- const createdAt = toIso(row.created_at, now);
536
- const updatedAt = toIso(row.updated_at, createdAt);
537
- return {
538
- thread_id: row.thread_id,
539
- status: row.status ?? "idle",
540
- metadata: row.metadata ?? {},
541
- values: row.values ?? {},
542
- interrupts: row.interrupts ?? {},
543
- created_at: createdAt,
544
- updated_at: updatedAt,
545
- state_updated_at: updatedAt
546
- };
547
- }
548
- function toRun(row, now) {
549
- const createdAt = toIso(row.created_at, now);
550
- const status = row.status ?? "success";
551
- return {
552
- run_id: row.run_id,
553
- thread_id: row.thread_id,
554
- assistant_id: row.assistant_id,
555
- status: isTerminalRunStatus(status) ? status : "error",
556
- metadata: row.metadata ?? {},
557
- multitask_strategy: row.multitask_strategy ?? null,
558
- created_at: createdAt,
559
- updated_at: toIso(row.updated_at, createdAt)
560
- };
370
+ return void 0;
561
371
  }
562
- function toRunKwargs(kwargs) {
563
- if (!kwargs) return {};
564
- const {
565
- input,
566
- command,
567
- config,
568
- context,
569
- stream_mode,
570
- interrupt_before,
571
- interrupt_after,
572
- webhook
573
- } = kwargs;
574
- return {
575
- input,
576
- command,
577
- config,
578
- context,
579
- stream_mode,
580
- interrupt_before,
581
- interrupt_after,
582
- ...typeof webhook === "string" ? { webhook } : {}
583
- };
372
+ function megabytes2(bytes) {
373
+ return `${Math.round(bytes / 1048576)}MB`;
584
374
  }
585
- async function readLanggraphDevState(langgraphApiDir) {
586
- const now = (/* @__PURE__ */ new Date()).toISOString();
587
- const [opsRaw, storeRaw, checkpointerRaw] = await Promise.all([
588
- readSuperjsonFile(path.join(langgraphApiDir, OPS_FILE)),
589
- readSuperjsonFile(path.join(langgraphApiDir, STORE_FILE)),
590
- readSuperjsonFile(path.join(langgraphApiDir, CHECKPOINTER_FILE))
591
- ]);
592
- if (!opsRaw && !storeRaw && !checkpointerRaw) return null;
593
- const ops = opsRaw ? validateShape(langgraphOpsSchema, opsRaw, OPS_FILE) : null;
594
- const storeFile = storeRaw ? validateShape(langgraphStoreFileSchema, storeRaw, STORE_FILE) : null;
595
- const checkpointerFile = checkpointerRaw ? validateShape(langgraphCheckpointerSchema, checkpointerRaw, CHECKPOINTER_FILE) : null;
596
- const runEntries = Object.entries(ops?.runs ?? {});
597
- const items = [];
598
- if (storeFile?.data) {
599
- for (const namespaceItems of storeFile.data.values()) {
600
- for (const stored of namespaceItems.values()) {
601
- const item = {
602
- namespace: stored.namespace,
603
- key: stored.key,
604
- value: stored.value,
605
- createdAt: toIso(stored.createdAt, now),
606
- updatedAt: toIso(stored.updatedAt, now)
607
- };
608
- items.push([JSON.stringify([stored.namespace, stored.key]), item]);
609
- }
610
- }
611
- }
612
- const store = {
613
- assistants: Object.entries(ops?.assistants ?? {}).map(([id, row]) => [
614
- id,
615
- toAssistant(row, now)
616
- ]),
617
- assistantVersions: (ops?.assistant_versions ?? []).map((row) => [
618
- JSON.stringify([row.assistant_id, row.version]),
619
- toAssistantVersion(row, now)
620
- ]),
621
- threads: Object.entries(ops?.threads ?? {}).map(([id, row]) => [id, toThread(row, now)]),
622
- runs: runEntries.map(([id, row]) => [id, toRun(row, now)]),
623
- runKwargs: runEntries.map(([id, row]) => [id, toRunKwargs(row.kwargs)]),
624
- items
625
- };
626
- let checkpoints = { storage: {}, writes: {} };
627
- if (checkpointerFile) {
628
- const saver = new MemorySaver3();
629
- saver.storage = checkpointerFile.storage;
630
- saver.writes = checkpointerFile.writes;
631
- checkpoints = snapshotCheckpointer(saver);
375
+ function describeHeapHeadroom(heapLimitBytes, containerLimitBytes) {
376
+ if (containerLimitBytes === void 0 || heapLimitBytes <= containerLimitBytes * HEAP_LIMIT_WARN_FRACTION) {
377
+ return { heapLimitBytes, containerLimitBytes, warning: void 0 };
632
378
  }
633
- return { version: 1, store, checkpoints };
634
- }
635
- function describeSnapshot(snapshot) {
379
+ const suggestedMb = Math.floor(containerLimitBytes * HEAP_LIMIT_SUGGEST_FRACTION / 1048576);
636
380
  return {
637
- assistants: snapshot.store.assistants.length,
638
- threads: snapshot.store.threads.length,
639
- runs: snapshot.store.runs.length,
640
- items: snapshot.store.items.length,
641
- checkpointedThreads: Object.keys(snapshot.checkpoints.storage).length
381
+ heapLimitBytes,
382
+ containerLimitBytes,
383
+ warning: `V8's heap limit (${megabytes2(heapLimitBytes)}) is close to or above this container's memory limit (${megabytes2(containerLimitBytes)}). Node's automatic sizing has a floor of about 259MB, so below roughly 345Mi it stops adapting \u2014 V8 may not run a full GC before the kernel OOM-kills the process, which looks like an unexplained restart rather than a memory error. Lower it explicitly with NODE_OPTIONS="--enable-source-maps --max-old-space-size=${suggestedMb}", or give the container more memory. Keep --enable-source-maps: NODE_OPTIONS replaces the image's value rather than adding to it, so setting it without that flag silently loses TypeScript stack traces.`
642
384
  };
643
385
  }
644
- function supportsRestore(store) {
645
- return typeof store.restore === "function";
646
- }
647
- async function copyCheckpoints(snapshot, target) {
648
- const source = new MemorySaver3();
649
- hydrateCheckpointer(source, snapshot);
650
- const tuples = [];
651
- for await (const tuple of source.list({})) tuples.push(tuple);
652
- tuples.reverse();
653
- for (const tuple of tuples) {
654
- const configurable = tuple.config.configurable ?? {};
655
- const putConfig = {
656
- configurable: {
657
- thread_id: configurable.thread_id,
658
- checkpoint_ns: configurable.checkpoint_ns ?? "",
659
- // `put` reads `checkpoint_id` as the PARENT pointer; the child's own id is `checkpoint.id`.
660
- checkpoint_id: tuple.parentConfig?.configurable?.checkpoint_id
661
- }
662
- };
663
- const stored = await target.put(
664
- putConfig,
665
- tuple.checkpoint,
666
- tuple.metadata ?? {},
667
- tuple.checkpoint.channel_versions ?? {}
668
- );
669
- const writesByTask = /* @__PURE__ */ new Map();
670
- for (const [taskId, channel, value] of tuple.pendingWrites ?? []) {
671
- const writes = writesByTask.get(taskId) ?? [];
672
- writes.push([channel, value]);
673
- writesByTask.set(taskId, writes);
674
- }
675
- for (const [taskId, writes] of writesByTask) {
676
- await target.putWrites(stored, writes, taskId);
677
- }
678
- }
386
+ function checkHeapHeadroom(options = {}) {
387
+ const heapLimit = (options.heapStatistics ?? getHeapStatistics2)().heap_size_limit;
388
+ return describeHeapHeadroom(heapLimit, readContainerMemoryLimit(options.readFile));
679
389
  }
680
- async function loadSnapshotIntoStore(snapshot, store, checkpointer) {
681
- if (!supportsRestore(store)) {
682
- throw new Error(
683
- "Target store does not support bulk import (no restore() method). Use the in-memory or Postgres driver, or implement restore() on your SkeinStore."
684
- );
685
- }
686
- await store.restore(snapshot.store);
687
- await copyCheckpoints(snapshot.checkpoints, checkpointer);
390
+
391
+ // src/index.ts
392
+ import { DEFAULT_RUN_CONCURRENCY as DEFAULT_RUN_CONCURRENCY2 } from "@skein-js/agent-protocol";
393
+ import { DEFAULT_SHUTDOWN_GRACE_MS as DEFAULT_SHUTDOWN_GRACE_MS2 } from "@skein-js/agent-protocol";
394
+
395
+ // src/route-matcher-cache.ts
396
+ import { createRouteMatcher } from "@skein-js/agent-protocol";
397
+ var matchers = /* @__PURE__ */ new WeakMap();
398
+ function routeMatcherFor(routes) {
399
+ const existing = matchers.get(routes);
400
+ if (existing) return existing;
401
+ const matcher = createRouteMatcher(routes);
402
+ matchers.set(routes, matcher);
403
+ return matcher;
688
404
  }
689
405
 
690
406
  // src/cors-runtime.ts
@@ -755,12 +471,15 @@ function sendNodePreflight(reqHeaders, res, cors) {
755
471
  }
756
472
 
757
473
  // src/node-transport.ts
758
- import { SSE_HEADERS } from "@skein-js/agent-protocol";
474
+ import {
475
+ SSE_HEADERS,
476
+ writeWithBackpressure
477
+ } from "@skein-js/agent-protocol";
759
478
  import { isSkeinHttpError, serializeWireJson } from "@skein-js/core";
760
479
  var ignoreStreamError = () => {
761
480
  };
762
- async function pipeServerSentEvents(status, events, res) {
763
- res.writeHead(status, SSE_HEADERS);
481
+ async function pipeServerSentEvents(status, events, res, headers = {}) {
482
+ res.writeHead(status, { ...SSE_HEADERS, ...headers });
764
483
  res.flushHeaders();
765
484
  const iterator = events[Symbol.asyncIterator]();
766
485
  let clientDisconnected = false;
@@ -774,7 +493,8 @@ async function pipeServerSentEvents(status, events, res) {
774
493
  for (; ; ) {
775
494
  const next = await iterator.next();
776
495
  if (next.done || clientDisconnected) break;
777
- res.write(next.value);
496
+ const drained = writeWithBackpressure(res, next.value);
497
+ if (drained) await drained;
778
498
  }
779
499
  } finally {
780
500
  res.removeListener("close", releaseOnClientClose);
@@ -784,14 +504,17 @@ async function pipeServerSentEvents(status, events, res) {
784
504
  async function sendNodeResponse(response, res) {
785
505
  switch (response.kind) {
786
506
  case "json":
787
- res.writeHead(response.status, { "content-type": "application/json" });
507
+ res.writeHead(response.status, {
508
+ "content-type": "application/json",
509
+ ...response.headers
510
+ });
788
511
  res.end(serializeWireJson(response.body));
789
512
  return;
790
513
  case "empty":
791
- res.writeHead(response.status).end();
514
+ res.writeHead(response.status, response.headers).end();
792
515
  return;
793
516
  case "sse":
794
- await pipeServerSentEvents(response.status, response.events, res);
517
+ await pipeServerSentEvents(response.status, response.events, res, response.headers);
795
518
  return;
796
519
  }
797
520
  }
@@ -831,35 +554,139 @@ function stripBasePath(pathname, basePath) {
831
554
  if (pathname.startsWith(`${prefix}/`)) return pathname.slice(prefix.length);
832
555
  return null;
833
556
  }
557
+
558
+ // src/runtime-capabilities.ts
559
+ function detectRuntimeCapabilities(scope = globalThis) {
560
+ const monotonic = () => scope.performance?.now() ?? Date.now();
561
+ const commonClock = { nowEpochMs: Date.now, nowMonotonicMs: monotonic };
562
+ if (scope.Bun) {
563
+ const bun = scope.Bun;
564
+ const process3 = scope.process;
565
+ if (!process3) throw new Error("Bun runtime does not expose process lifecycle compatibility.");
566
+ return {
567
+ name: "bun",
568
+ version: bun.version,
569
+ env: {
570
+ get: (name) => bun.env[name],
571
+ set: (name, value) => {
572
+ bun.env[name] = value;
573
+ }
574
+ },
575
+ clock: commonClock,
576
+ memory: {
577
+ read: () => {
578
+ const native = bun.memoryUsage?.();
579
+ const fallback = process3.memoryUsage();
580
+ return {
581
+ rssBytes: native?.rss ?? fallback.rss,
582
+ heapUsedBytes: native?.heapUsed ?? fallback.heapUsed,
583
+ nativeHeapBytes: native?.current
584
+ };
585
+ }
586
+ },
587
+ signals: {
588
+ on: (signal, listener) => {
589
+ process3.on(signal, listener);
590
+ return { dispose: () => process3.off(signal, listener) };
591
+ }
592
+ },
593
+ exit: (code) => process3.exit(code)
594
+ };
595
+ }
596
+ if (scope.Deno) {
597
+ const deno = scope.Deno;
598
+ return {
599
+ name: "deno",
600
+ version: deno.version.deno,
601
+ env: deno.env,
602
+ clock: commonClock,
603
+ memory: {
604
+ read: () => {
605
+ const memory = deno.memoryUsage();
606
+ return { rssBytes: memory.rss, heapUsedBytes: memory.heapUsed };
607
+ }
608
+ },
609
+ signals: {
610
+ on: (signal, listener) => {
611
+ deno.addSignalListener(signal, listener);
612
+ return { dispose: () => deno.removeSignalListener(signal, listener) };
613
+ }
614
+ },
615
+ exit: (code) => deno.exit(code)
616
+ };
617
+ }
618
+ const process2 = scope.process;
619
+ if (!process2)
620
+ throw new Error("Unsupported JavaScript runtime: process, Bun, and Deno are absent.");
621
+ return {
622
+ name: "node",
623
+ version: process2.version.replace(/^v/, ""),
624
+ env: {
625
+ get: (name) => process2.env[name],
626
+ set: (name, value) => {
627
+ process2.env[name] = value;
628
+ }
629
+ },
630
+ clock: commonClock,
631
+ memory: {
632
+ read: () => {
633
+ const memory = process2.memoryUsage();
634
+ return { rssBytes: memory.rss, heapUsedBytes: memory.heapUsed };
635
+ }
636
+ },
637
+ signals: {
638
+ on: (signal, listener) => {
639
+ process2.on(signal, listener);
640
+ return { dispose: () => process2.off(signal, listener) };
641
+ }
642
+ },
643
+ exit: (code) => process2.exit(code)
644
+ };
645
+ }
834
646
  export {
647
+ DEFAULT_HEAP_SAMPLE_MS,
648
+ DEFAULT_HEAP_WARN_PERCENT,
835
649
  DEFAULT_RUN_CONCURRENCY2 as DEFAULT_RUN_CONCURRENCY,
836
650
  DEFAULT_SHUTDOWN_GRACE_MS2 as DEFAULT_SHUTDOWN_GRACE_MS,
651
+ HEAP_REARM_MARGIN_PERCENT,
652
+ HEAP_REARM_PERCENT,
837
653
  allowedOrigin,
838
654
  applyNodeCors,
655
+ checkHeapHeadroom,
839
656
  corsFromHttpConfig,
840
657
  corsPreflightHeaders,
841
658
  corsResponseHeaders,
842
659
  createConsoleLogger,
843
660
  createInMemoryDeps,
844
661
  describeError,
845
- describeSnapshot,
662
+ describeHeapHeadroom,
663
+ describePoolPressure,
664
+ detectRuntimeCapabilities,
665
+ disabledRoutesFromHttpConfig,
846
666
  embedInMemoryGraphs,
847
667
  formatLogMeta,
848
668
  graphMapToResolver,
849
669
  joinList,
850
670
  loadInMemoryRuntime,
851
671
  loadReloadableInMemoryRuntime,
852
- loadSnapshotIntoStore,
853
672
  normalizeEmbeddableGraphs,
854
- readLanggraphDevState,
673
+ readContainerMemoryLimit,
674
+ resolveCronTickMs,
675
+ resolveHeapPressureOptions,
676
+ resolveMaxPageSize,
677
+ resolveMemoryBusLimits,
855
678
  resolveProtocolRuntime,
856
679
  resolveRunConcurrency,
680
+ resolveRunTimeoutMs,
857
681
  resolveRuntimeDeps,
858
682
  resolveShutdownGraceMs,
683
+ routeMatcherFor,
684
+ routesFromHttpConfig,
859
685
  runFailureIdentity,
860
686
  sendNodeError,
861
687
  sendNodePreflight,
862
688
  sendNodeResponse,
689
+ startHeapPressureMonitor,
863
690
  stripBasePath,
864
691
  toCorsOptions
865
692
  };