@skein-js/express 0.2.1 → 0.4.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.d.ts CHANGED
@@ -4,6 +4,8 @@ import { CorsOptions } from 'cors';
4
4
  import { Router, Express, Request, Response } from 'express';
5
5
  import { Server } from 'node:http';
6
6
  import { MemoryStoreSnapshot } from '@skein-js/storage-memory';
7
+ import { BaseCheckpointSaver } from '@langchain/langgraph';
8
+ import { SkeinStore } from '@skein-js/core';
7
9
 
8
10
  interface SkeinRouterCommonOptions {
9
11
  logger?: Logger;
@@ -150,6 +152,30 @@ interface ReloadableInMemoryRuntime extends InMemoryRuntimeConfig {
150
152
  */
151
153
  declare function loadReloadableInMemoryRuntime(configPath: string, importModule?: ModuleImporter): Promise<ReloadableInMemoryRuntime>;
152
154
 
155
+ /**
156
+ * Read a LangGraph `.langgraph_api/` directory and reconstruct skein's `DevStateSnapshot`.
157
+ * Returns `null` when the directory holds none of the expected files (nothing to import).
158
+ */
159
+ declare function readLanggraphDevState(langgraphApiDir: string): Promise<DevStateSnapshot | null>;
160
+ /** Row/checkpoint counts in a snapshot, for CLI + log summaries. */
161
+ interface DevStateCounts {
162
+ assistants: number;
163
+ threads: number;
164
+ runs: number;
165
+ items: number;
166
+ /** Threads that carry graph checkpoint history. */
167
+ checkpointedThreads: number;
168
+ }
169
+ declare function describeSnapshot(snapshot: DevStateSnapshot): DevStateCounts;
170
+ /**
171
+ * Load a `DevStateSnapshot` into a live store + checkpointer — the sink for importing into a real
172
+ * skein deployment (e.g. Postgres). Resource rows go through the driver's `restore` (ids +
173
+ * timestamps preserved, `ON CONFLICT DO NOTHING` so re-runs are safe); checkpoints — graph state +
174
+ * full history — are copied via the public checkpointer API. Throws if the store can't bulk-restore
175
+ * (both first-party drivers can); a custom driver must implement `restore` to be an import target.
176
+ */
177
+ declare function loadSnapshotIntoStore(snapshot: DevStateSnapshot, store: SkeinStore, checkpointer: BaseCheckpointSaver): Promise<void>;
178
+
153
179
  /** The `http.cors` block of a langgraph.json — LangGraph's field names (snake_case). */
154
180
  interface LanggraphCorsConfig {
155
181
  allow_origins?: string[];
@@ -165,4 +191,4 @@ declare function toCorsOptions(config: LanggraphCorsConfig): CorsOptions;
165
191
  /** Read `http.cors` from a langgraph.json `http` block, mapped to `CorsOptions`, or `undefined`. */
166
192
  declare function corsFromHttpConfig(http: unknown): CorsOptions | undefined;
167
193
 
168
- export { type DevStateSnapshot, type HandlerRouterOptions, type InMemoryRuntimeConfig, type LanggraphCorsConfig, type ReloadableInMemoryRuntime, type SkeinExpressServer, type SkeinRouter, type SkeinRouterOptions, corsFromHttpConfig, createExpressServer, createHandlerRouter, loadInMemoryRuntime, loadReloadableInMemoryRuntime, sendErrorResponse, sendProtocolResponse, skeinRouter, skeinRoutes, toCorsOptions, toProtocolRequest };
194
+ export { type DevStateCounts, type DevStateSnapshot, type HandlerRouterOptions, type InMemoryRuntimeConfig, type LanggraphCorsConfig, type ReloadableInMemoryRuntime, type SkeinExpressServer, type SkeinRouter, type SkeinRouterOptions, corsFromHttpConfig, createExpressServer, createHandlerRouter, describeSnapshot, loadInMemoryRuntime, loadReloadableInMemoryRuntime, loadSnapshotIntoStore, readLanggraphDevState, sendErrorResponse, sendProtocolResponse, skeinRouter, skeinRoutes, toCorsOptions, toProtocolRequest };
package/dist/index.js CHANGED
@@ -253,6 +253,7 @@ var skeinRoutes = [
253
253
  // threads
254
254
  { method: "post", path: "/threads/search", handler: "listThreads" },
255
255
  { method: "post", path: "/threads", handler: "createThread" },
256
+ { method: "post", path: "/threads/:thread_id/copy", handler: "copyThread" },
256
257
  { method: "get", path: "/threads/:thread_id", handler: "getThread" },
257
258
  { method: "patch", path: "/threads/:thread_id", handler: "patchThread" },
258
259
  { method: "delete", path: "/threads/:thread_id", handler: "deleteThread" },
@@ -306,7 +307,9 @@ function createHandlerRouter(handlers, options = {}) {
306
307
  router[binding.method](binding.path, async (req, res) => {
307
308
  try {
308
309
  const request = toProtocolRequest(req);
309
- const response = await invoke(binding.foldThreadIdIntoBody ? foldThreadId(request) : request);
310
+ const response = await invoke(
311
+ binding.foldThreadIdIntoBody ? foldThreadId(request) : request
312
+ );
310
313
  await sendProtocolResponse(response, res);
311
314
  } catch (error) {
312
315
  sendErrorResponse(error, res, options.logger);
@@ -352,11 +355,14 @@ import express2 from "express";
352
355
  function requestLogger(logger) {
353
356
  return (req, res, next) => {
354
357
  const startedAt = Date.now();
358
+ logger.info(`<-- ${req.method} ${req.originalUrl}`);
355
359
  let logged = false;
356
360
  const log = () => {
357
361
  if (logged) return;
358
362
  logged = true;
359
- logger.info(`${req.method} ${req.originalUrl} ${res.statusCode} ${Date.now() - startedAt}ms`);
363
+ logger.info(
364
+ `--> ${req.method} ${req.originalUrl} ${res.statusCode} ${Date.now() - startedAt}ms`
365
+ );
360
366
  };
361
367
  res.once("finish", log);
362
368
  res.once("close", log);
@@ -367,6 +373,9 @@ async function createExpressServer(options) {
367
373
  const { router, runtime } = await skeinRouter(options);
368
374
  const app = express2();
369
375
  if (options.logger) app.use(requestLogger(options.logger));
376
+ app.get("/ok", (_req, res) => {
377
+ res.json({ ok: true });
378
+ });
370
379
  app.use(router);
371
380
  let server;
372
381
  return {
@@ -389,12 +398,262 @@ async function createExpressServer(options) {
389
398
  }
390
399
  };
391
400
  }
401
+
402
+ // src/langgraph-import.ts
403
+ import { readFile } from "fs/promises";
404
+ import path from "path";
405
+ import {
406
+ MemorySaver as MemorySaver2
407
+ } from "@langchain/langgraph";
408
+ import {
409
+ isTerminalRunStatus
410
+ } from "@skein-js/core";
411
+ import { SuperJSON } from "superjson";
412
+ import { z } from "zod";
413
+ var OPS_FILE = ".langgraphjs_ops.json";
414
+ var STORE_FILE = ".langgraphjs_api.store.json";
415
+ var CHECKPOINTER_FILE = ".langgraphjs_api.checkpointer.json";
416
+ var langgraphSuperjson = new SuperJSON();
417
+ langgraphSuperjson.registerCustom(
418
+ {
419
+ isApplicable: (value) => value instanceof Uint8Array,
420
+ serialize: (value) => Buffer.from(value).toString("base64"),
421
+ deserialize: (value) => new Uint8Array(Buffer.from(value, "base64"))
422
+ },
423
+ "Uint8Array"
424
+ );
425
+ var timestamp = z.union([z.date(), z.string()]).optional();
426
+ var jsonObject = z.record(z.unknown());
427
+ var langgraphAssistantSchema = z.object({
428
+ assistant_id: z.string(),
429
+ graph_id: z.string(),
430
+ name: z.string().optional(),
431
+ description: z.string().nullish(),
432
+ config: jsonObject.optional(),
433
+ context: z.unknown().optional(),
434
+ metadata: jsonObject.optional(),
435
+ version: z.number().optional(),
436
+ created_at: timestamp,
437
+ updated_at: timestamp
438
+ }).passthrough();
439
+ var langgraphThreadSchema = z.object({
440
+ thread_id: z.string(),
441
+ status: z.string().optional(),
442
+ metadata: jsonObject.optional(),
443
+ values: jsonObject.optional(),
444
+ interrupts: jsonObject.optional(),
445
+ created_at: timestamp,
446
+ updated_at: timestamp
447
+ }).passthrough();
448
+ var langgraphRunSchema = z.object({
449
+ run_id: z.string(),
450
+ thread_id: z.string(),
451
+ assistant_id: z.string(),
452
+ status: z.string().optional(),
453
+ metadata: jsonObject.optional(),
454
+ multitask_strategy: z.string().nullish(),
455
+ kwargs: jsonObject.optional(),
456
+ created_at: timestamp,
457
+ updated_at: timestamp
458
+ }).passthrough();
459
+ var langgraphOpsSchema = z.object({
460
+ assistants: z.record(langgraphAssistantSchema).optional(),
461
+ threads: z.record(langgraphThreadSchema).optional(),
462
+ runs: z.record(langgraphRunSchema).optional()
463
+ }).passthrough();
464
+ var langgraphStoreItemSchema = z.object({
465
+ namespace: z.array(z.string()),
466
+ key: z.string(),
467
+ value: jsonObject,
468
+ createdAt: timestamp,
469
+ updatedAt: timestamp
470
+ }).passthrough();
471
+ var langgraphStoreFileSchema = z.object({ data: z.map(z.string(), z.map(z.string(), langgraphStoreItemSchema)).optional() }).passthrough();
472
+ var langgraphCheckpointerSchema = z.object({ storage: jsonObject, writes: jsonObject }).passthrough();
473
+ async function readSuperjsonFile(filepath) {
474
+ let text;
475
+ try {
476
+ text = await readFile(filepath, "utf8");
477
+ } catch {
478
+ return null;
479
+ }
480
+ try {
481
+ return langgraphSuperjson.parse(text) ?? null;
482
+ } catch (error) {
483
+ throw new Error(
484
+ `Could not parse LangGraph state file "${filepath}": ${error instanceof Error ? error.message : String(error)}`
485
+ );
486
+ }
487
+ }
488
+ function validateShape(schema, value, label) {
489
+ const result = schema.safeParse(value);
490
+ if (!result.success) {
491
+ const detail = result.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
492
+ throw new Error(`Invalid LangGraph ${label}: ${detail}`);
493
+ }
494
+ return result.data;
495
+ }
496
+ function toIso(value, fallback) {
497
+ if (value instanceof Date) return value.toISOString();
498
+ if (typeof value === "string") return value;
499
+ return fallback;
500
+ }
501
+ function toAssistant(row, now) {
502
+ return {
503
+ assistant_id: row.assistant_id,
504
+ graph_id: row.graph_id,
505
+ name: row.name ?? row.graph_id,
506
+ description: row.description ?? void 0,
507
+ config: row.config ?? {},
508
+ context: row.context ?? {},
509
+ metadata: row.metadata ?? {},
510
+ version: row.version ?? 1,
511
+ created_at: toIso(row.created_at, now),
512
+ updated_at: toIso(row.updated_at, now)
513
+ };
514
+ }
515
+ function toThread(row, now) {
516
+ const createdAt = toIso(row.created_at, now);
517
+ const updatedAt = toIso(row.updated_at, createdAt);
518
+ return {
519
+ thread_id: row.thread_id,
520
+ status: row.status ?? "idle",
521
+ metadata: row.metadata ?? {},
522
+ values: row.values ?? {},
523
+ interrupts: row.interrupts ?? {},
524
+ created_at: createdAt,
525
+ updated_at: updatedAt,
526
+ state_updated_at: updatedAt
527
+ };
528
+ }
529
+ function toRun(row, now) {
530
+ const createdAt = toIso(row.created_at, now);
531
+ const status = row.status ?? "success";
532
+ return {
533
+ run_id: row.run_id,
534
+ thread_id: row.thread_id,
535
+ assistant_id: row.assistant_id,
536
+ status: isTerminalRunStatus(status) ? status : "error",
537
+ metadata: row.metadata ?? {},
538
+ multitask_strategy: row.multitask_strategy ?? null,
539
+ created_at: createdAt,
540
+ updated_at: toIso(row.updated_at, createdAt)
541
+ };
542
+ }
543
+ function toRunKwargs(kwargs) {
544
+ if (!kwargs) return {};
545
+ const { input, command, config, context, stream_mode, interrupt_before, interrupt_after } = kwargs;
546
+ return { input, command, config, context, stream_mode, interrupt_before, interrupt_after };
547
+ }
548
+ async function readLanggraphDevState(langgraphApiDir) {
549
+ const now = (/* @__PURE__ */ new Date()).toISOString();
550
+ const [opsRaw, storeRaw, checkpointerRaw] = await Promise.all([
551
+ readSuperjsonFile(path.join(langgraphApiDir, OPS_FILE)),
552
+ readSuperjsonFile(path.join(langgraphApiDir, STORE_FILE)),
553
+ readSuperjsonFile(path.join(langgraphApiDir, CHECKPOINTER_FILE))
554
+ ]);
555
+ if (!opsRaw && !storeRaw && !checkpointerRaw) return null;
556
+ const ops = opsRaw ? validateShape(langgraphOpsSchema, opsRaw, OPS_FILE) : null;
557
+ const storeFile = storeRaw ? validateShape(langgraphStoreFileSchema, storeRaw, STORE_FILE) : null;
558
+ const checkpointerFile = checkpointerRaw ? validateShape(langgraphCheckpointerSchema, checkpointerRaw, CHECKPOINTER_FILE) : null;
559
+ const runEntries = Object.entries(ops?.runs ?? {});
560
+ const items = [];
561
+ if (storeFile?.data) {
562
+ for (const namespaceItems of storeFile.data.values()) {
563
+ for (const stored of namespaceItems.values()) {
564
+ const item = {
565
+ namespace: stored.namespace,
566
+ key: stored.key,
567
+ value: stored.value,
568
+ createdAt: toIso(stored.createdAt, now),
569
+ updatedAt: toIso(stored.updatedAt, now)
570
+ };
571
+ items.push([JSON.stringify([stored.namespace, stored.key]), item]);
572
+ }
573
+ }
574
+ }
575
+ const store = {
576
+ assistants: Object.entries(ops?.assistants ?? {}).map(([id, row]) => [
577
+ id,
578
+ toAssistant(row, now)
579
+ ]),
580
+ threads: Object.entries(ops?.threads ?? {}).map(([id, row]) => [id, toThread(row, now)]),
581
+ runs: runEntries.map(([id, row]) => [id, toRun(row, now)]),
582
+ runKwargs: runEntries.map(([id, row]) => [id, toRunKwargs(row.kwargs)]),
583
+ items
584
+ };
585
+ let checkpoints = { storage: {}, writes: {} };
586
+ if (checkpointerFile) {
587
+ const saver = new MemorySaver2();
588
+ saver.storage = checkpointerFile.storage;
589
+ saver.writes = checkpointerFile.writes;
590
+ checkpoints = snapshotCheckpointer(saver);
591
+ }
592
+ return { version: 1, store, checkpoints };
593
+ }
594
+ function describeSnapshot(snapshot) {
595
+ return {
596
+ assistants: snapshot.store.assistants.length,
597
+ threads: snapshot.store.threads.length,
598
+ runs: snapshot.store.runs.length,
599
+ items: snapshot.store.items.length,
600
+ checkpointedThreads: Object.keys(snapshot.checkpoints.storage).length
601
+ };
602
+ }
603
+ function supportsRestore(store) {
604
+ return typeof store.restore === "function";
605
+ }
606
+ async function copyCheckpoints(snapshot, target) {
607
+ const source = new MemorySaver2();
608
+ hydrateCheckpointer(source, snapshot);
609
+ const tuples = [];
610
+ for await (const tuple of source.list({})) tuples.push(tuple);
611
+ tuples.reverse();
612
+ for (const tuple of tuples) {
613
+ const configurable = tuple.config.configurable ?? {};
614
+ const putConfig = {
615
+ configurable: {
616
+ thread_id: configurable.thread_id,
617
+ checkpoint_ns: configurable.checkpoint_ns ?? "",
618
+ // `put` reads `checkpoint_id` as the PARENT pointer; the child's own id is `checkpoint.id`.
619
+ checkpoint_id: tuple.parentConfig?.configurable?.checkpoint_id
620
+ }
621
+ };
622
+ const stored = await target.put(
623
+ putConfig,
624
+ tuple.checkpoint,
625
+ tuple.metadata ?? {},
626
+ tuple.checkpoint.channel_versions ?? {}
627
+ );
628
+ const writesByTask = /* @__PURE__ */ new Map();
629
+ for (const [taskId, channel, value] of tuple.pendingWrites ?? []) {
630
+ const writes = writesByTask.get(taskId) ?? [];
631
+ writes.push([channel, value]);
632
+ writesByTask.set(taskId, writes);
633
+ }
634
+ for (const [taskId, writes] of writesByTask) {
635
+ await target.putWrites(stored, writes, taskId);
636
+ }
637
+ }
638
+ }
639
+ async function loadSnapshotIntoStore(snapshot, store, checkpointer) {
640
+ if (!supportsRestore(store)) {
641
+ throw new Error(
642
+ "Target store does not support bulk import (no restore() method). Use the in-memory or Postgres driver, or implement restore() on your SkeinStore."
643
+ );
644
+ }
645
+ await store.restore(snapshot.store);
646
+ await copyCheckpoints(snapshot.checkpoints, checkpointer);
647
+ }
392
648
  export {
393
649
  corsFromHttpConfig,
394
650
  createExpressServer,
395
651
  createHandlerRouter,
652
+ describeSnapshot,
396
653
  loadInMemoryRuntime,
397
654
  loadReloadableInMemoryRuntime,
655
+ loadSnapshotIntoStore,
656
+ readLanggraphDevState,
398
657
  sendErrorResponse,
399
658
  sendProtocolResponse,
400
659
  skeinRouter,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skein-js/express",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "Express adapter for skein-js — mount the Agent Protocol on an Express Router.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Maina Wycliffe <wmmaina7@gmail.com>",
@@ -41,10 +41,12 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "cors": "^2.8.5",
44
- "@skein-js/agent-protocol": "0.2.1",
45
- "@skein-js/core": "0.2.1",
46
- "@skein-js/storage-memory": "0.2.1",
47
- "@skein-js/config": "0.2.1"
44
+ "superjson": "^2.2.6",
45
+ "zod": "^3.25.76",
46
+ "@skein-js/agent-protocol": "0.4.0",
47
+ "@skein-js/config": "0.4.0",
48
+ "@skein-js/storage-memory": "0.4.0",
49
+ "@skein-js/core": "0.4.0"
48
50
  },
49
51
  "peerDependencies": {
50
52
  "@langchain/langgraph": "^1.4.0",