@skein-js/runtime 0.3.0 → 0.5.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/README.md CHANGED
@@ -20,8 +20,8 @@ import { createExpressServer } from "@skein-js/express";
20
20
 
21
21
  const runtime = await buildRuntime({
22
22
  configPath: "/abs/path/to/langgraph.json",
23
- store: "postgres", // "memory" | "postgres" (postgres reads DATABASE_URL)
24
- queue: "redis", // "memory" | "redis" (redis reads REDIS_URL)
23
+ store: "postgres", // "memory" | "postgres" (postgres reads POSTGRES_URI)
24
+ queue: "redis", // "memory" | "redis" (redis reads REDIS_URI)
25
25
  });
26
26
 
27
27
  const server = await createExpressServer({ deps: runtime.deps, cors: runtime.cors });
@@ -33,13 +33,13 @@ await runtime.dispose();
33
33
  `store` and `queue` are **required** (no defaults — the CLI supplies its own flag defaults). The
34
34
  driver branches:
35
35
 
36
- - **`store: "postgres"`** connects `PostgresSkeinStore` (from `DATABASE_URL`), runs its migrations,
36
+ - **`store: "postgres"`** connects `PostgresSkeinStore` (from `POSTGRES_URI`), runs its migrations,
37
37
  and uses `PostgresSaver` as the LangGraph checkpointer.
38
- - **`queue: "redis"`** uses the BullMQ run queue + Redis Streams/pub-sub event bus (from `REDIS_URL`).
38
+ - **`queue: "redis"`** uses the BullMQ run queue + Redis Streams/pub-sub event bus (from `REDIS_URI`).
39
39
  - **`store: "memory"` + `queue: "memory"`** delegates to [`@skein-js/express`](../server-express)'s
40
40
  reloadable in-memory runtime, so `skein dev`'s hot-reload and cross-restart state persistence work.
41
41
 
42
- A missing `DATABASE_URL` / `REDIS_URL` throws `RuntimeConfigError`; if assembly fails part-way, any
42
+ A missing `POSTGRES_URI` / `REDIS_URI` throws `RuntimeConfigError`; if assembly fails part-way, any
43
43
  resources already created are disposed before rethrowing, so a failed build leaks nothing.
44
44
 
45
45
  Graph hot-reload (`reloadGraphs()`) works in every mode; `snapshotState`/`hydrateState` are present
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ProtocolDeps } from '@skein-js/agent-protocol';
2
- import { ModuleImporter } from '@skein-js/config';
2
+ import { ModuleImporter, GraphSchemas } from '@skein-js/config';
3
3
  import { DevStateSnapshot } from '@skein-js/express';
4
4
  import { CorsOptions } from 'cors';
5
5
  import { EmbedFunction } from '@skein-js/storage-postgres';
@@ -13,9 +13,14 @@ interface BuildRuntimeOptions {
13
13
  configPath: string;
14
14
  /** TS-capable importer (e.g. the CLI's vite loader). Omitted for plain JS/Node resolution. */
15
15
  importModule?: ModuleImporter;
16
- /** `"memory"` (default dev) or `"postgres"` (reads `DATABASE_URL`). */
16
+ /**
17
+ * Precomputed graph schemas (from `skein build`) for a pre-compiled image — forwarded to
18
+ * `loadConfig` so schema introspection is a map lookup, never a TypeScript parse. Omitted for dev.
19
+ */
20
+ schemas?: Record<string, GraphSchemas>;
21
+ /** `"memory"` (default dev) or `"postgres"` (reads `POSTGRES_URI`). */
17
22
  store: StoreDriver;
18
- /** `"memory"` (default dev) or `"redis"` (reads `REDIS_URL`). */
23
+ /** `"memory"` (default dev) or `"redis"` (reads `REDIS_URI`). */
19
24
  queue: QueueDriver;
20
25
  }
21
26
  interface SkeinRuntime {
@@ -46,7 +51,21 @@ interface ResolveEmbedOptions {
46
51
  /** TS-capable importer (the CLI's vite loader) for a custom-function `.ts` path. */
47
52
  importModule?: ModuleImporter;
48
53
  }
54
+ /**
55
+ * Whether a `store.index.embed` value is a custom-function path (`"./embed.ts:fn"`) rather than a
56
+ * `"provider:model"` string — it looks like a path (starts with `.`/`/`) or names a source file.
57
+ * Exported so `skein build` knows which embed forms to bundle (paths) vs leave as provider strings.
58
+ */
59
+ declare function isCustomFunctionPath(embed: string): boolean;
60
+ /**
61
+ * The npm package a `store.index.embed` value needs **installed at runtime**, or undefined. A
62
+ * `provider:model` embed dynamically imports `@langchain/<provider>` (see {@link resolveProviderEmbed}),
63
+ * but that package is never imported by graph code, so `skein build` won't discover it while bundling —
64
+ * it must pin this package into the production image explicitly. Custom-function paths return undefined
65
+ * (they are bundled), as do unknown providers.
66
+ */
67
+ declare function embedRuntimePackage(embed: string): string | undefined;
49
68
  /** Resolve a `store.index.embed` value to an `EmbedFunction`, honoring both LangGraph forms. */
50
69
  declare function resolveEmbed(embed: string, options: ResolveEmbedOptions): Promise<EmbedFunction>;
51
70
 
52
- export { type BuildRuntimeOptions, type QueueDriver, type ResolveEmbedOptions, RuntimeConfigError, type SkeinRuntime, type StoreDriver, buildRuntime, resolveEmbed };
71
+ export { type BuildRuntimeOptions, type QueueDriver, type ResolveEmbedOptions, RuntimeConfigError, type SkeinRuntime, type StoreDriver, buildRuntime, embedRuntimePackage, isCustomFunctionPath, resolveEmbed };
package/dist/index.js CHANGED
@@ -52,6 +52,12 @@ function isCustomFunctionPath(embed) {
52
52
  const head = embed.split(":", 1)[0] ?? "";
53
53
  return head.startsWith(".") || head.startsWith("/") || /\.([mc]?[jt]s|py)$/.test(head);
54
54
  }
55
+ function embedRuntimePackage(embed) {
56
+ if (isCustomFunctionPath(embed)) return void 0;
57
+ const separator = embed.indexOf(":");
58
+ if (separator === -1) return void 0;
59
+ return PROVIDERS[embed.slice(0, separator)]?.package;
60
+ }
55
61
  async function resolvePathEmbed(embed, options) {
56
62
  const { sourceFile, exportSymbol } = parseGraphSpec(embed, options.configDir);
57
63
  const importer = options.importModule ?? ((file) => import(pathToFileURL(file).href));
@@ -145,9 +151,9 @@ async function resolveStoreIndex(index, options) {
145
151
  return { dims: index.dims, fields: index.fields, embed };
146
152
  }
147
153
  async function buildRuntime(options) {
148
- const { configPath, importModule, store, queue } = options;
154
+ const { configPath, importModule, store, queue, schemas } = options;
149
155
  if (store === "memory" && queue === "memory") {
150
- const runtime = await loadReloadableInMemoryRuntime(configPath, importModule);
156
+ const runtime = await loadReloadableInMemoryRuntime(configPath, importModule, schemas);
151
157
  return {
152
158
  deps: runtime.deps,
153
159
  cors: runtime.cors,
@@ -158,16 +164,17 @@ async function buildRuntime(options) {
158
164
  hydrateState: (snapshot) => runtime.hydrateState(snapshot)
159
165
  };
160
166
  }
161
- const first = await loadConfig({ configPath, importModule });
167
+ const first = await loadConfig({ configPath, importModule, staticSchemas: schemas });
162
168
  const { resolver, reroute } = reroutableGraphResolver(first.graphs);
163
169
  const disposers = [];
164
170
  const disposeAll = async () => {
165
171
  await Promise.allSettled(disposers.map((dispose) => dispose()));
166
172
  };
167
173
  try {
174
+ const storeTtl = resolveStoreTtl(first.config.store?.ttl);
168
175
  const { skeinStore, checkpointer } = await (async () => {
169
176
  if (store === "postgres") {
170
- const databaseUrl = requireEnv("DATABASE_URL", "postgres");
177
+ const databaseUrl = requireEnv("POSTGRES_URI", "postgres");
171
178
  const index = await resolveStoreIndex(first.config.store?.index, {
172
179
  configDir: first.configDir,
173
180
  importModule
@@ -175,6 +182,7 @@ async function buildRuntime(options) {
175
182
  const connectionOptions = postgresConnectionOptions();
176
183
  const pgStore = await PostgresSkeinStore.connect(databaseUrl, {
177
184
  ...index ? { index } : {},
185
+ ...storeTtl ? { ttl: storeTtl } : {},
178
186
  ...connectionOptions
179
187
  });
180
188
  disposers.push(() => pgStore.close());
@@ -184,11 +192,24 @@ async function buildRuntime(options) {
184
192
  await saver.setup();
185
193
  return { skeinStore: pgStore, checkpointer: saver };
186
194
  }
187
- return { skeinStore: new MemorySkeinStore(), checkpointer: new MemorySaver() };
195
+ return {
196
+ skeinStore: new MemorySkeinStore(storeTtl ? { ttl: storeTtl } : void 0),
197
+ checkpointer: new MemorySaver()
198
+ };
188
199
  })();
200
+ if (storeTtl) {
201
+ const everyMs = (storeTtl.sweepIntervalMinutes ?? 60) * 6e4;
202
+ const sweeper = setInterval(() => {
203
+ skeinStore.store.sweepExpired().catch((error) => {
204
+ console.error("skein: store TTL sweep failed", error);
205
+ });
206
+ }, everyMs);
207
+ sweeper.unref();
208
+ disposers.push(async () => clearInterval(sweeper));
209
+ }
189
210
  const { runQueue, bus } = (() => {
190
211
  if (queue === "redis") {
191
- const redisUrl = requireEnv("REDIS_URL", "redis");
212
+ const redisUrl = requireEnv("REDIS_URI", "redis");
192
213
  const redisQueue = new RedisRunQueue(redisUrl);
193
214
  disposers.push(() => redisQueue.dispose());
194
215
  const redisBus = new RedisRunEventBus(redisUrl);
@@ -212,7 +233,7 @@ async function buildRuntime(options) {
212
233
  deps,
213
234
  cors: corsFromHttpConfig(first.config.http),
214
235
  reloadGraphs: async () => {
215
- reroute((await loadConfig({ configPath, importModule })).graphs);
236
+ reroute((await loadConfig({ configPath, importModule, staticSchemas: schemas })).graphs);
216
237
  },
217
238
  dispose: disposeAll
218
239
  };
@@ -221,8 +242,20 @@ async function buildRuntime(options) {
221
242
  throw error;
222
243
  }
223
244
  }
245
+ function resolveStoreTtl(raw) {
246
+ if (!raw) return void 0;
247
+ const ttl = {};
248
+ if (typeof raw.default_ttl === "number") ttl.defaultTtl = raw.default_ttl;
249
+ if (typeof raw.refresh_on_read === "boolean") ttl.refreshOnRead = raw.refresh_on_read;
250
+ if (typeof raw.sweep_interval_minutes === "number") {
251
+ ttl.sweepIntervalMinutes = raw.sweep_interval_minutes;
252
+ }
253
+ return Object.keys(ttl).length > 0 ? ttl : void 0;
254
+ }
224
255
  export {
225
256
  RuntimeConfigError,
226
257
  buildRuntime,
258
+ embedRuntimePackage,
259
+ isCustomFunctionPath,
227
260
  resolveEmbed
228
261
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skein-js/runtime",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Assembles skein-js ProtocolDeps from langgraph.json + selected drivers (memory/Postgres/Redis).",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Maina Wycliffe <wmmaina7@gmail.com>",
@@ -42,12 +42,12 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "cors": "^2.8.5",
45
- "@skein-js/agent-protocol": "0.3.0",
46
- "@skein-js/express": "0.3.0",
47
- "@skein-js/config": "0.3.0",
48
- "@skein-js/storage-memory": "0.3.0",
49
- "@skein-js/storage-postgres": "0.3.0",
50
- "@skein-js/redis": "0.3.0"
45
+ "@skein-js/agent-protocol": "0.5.0",
46
+ "@skein-js/express": "0.5.0",
47
+ "@skein-js/config": "0.5.0",
48
+ "@skein-js/redis": "0.5.0",
49
+ "@skein-js/storage-memory": "0.5.0",
50
+ "@skein-js/storage-postgres": "0.5.0"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "@langchain/langgraph": "^1.4.0",
@@ -58,7 +58,7 @@
58
58
  "@langchain/langgraph": "^1.4.0",
59
59
  "@langchain/langgraph-checkpoint-postgres": "^1.0.4",
60
60
  "@types/cors": "^2.8.17",
61
- "@skein-js/test-support": "0.3.0"
61
+ "@skein-js/test-support": "0.5.0"
62
62
  },
63
63
  "publishConfig": {
64
64
  "access": "public"