@skein-js/express 0.2.0 → 0.3.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
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Express adapter for skein-js — mount the Agent Protocol on an Express `Router`.
4
4
 
5
- Part of **[skein-js](https://github.com/mainawycliffe/skein)** — a TypeScript [Agent Protocol](https://github.com/langchain-ai/agent-protocol) server for [LangGraph.js](https://github.com/langchain-ai/langgraphjs), and a drop-in replacement for the LangGraph CLI.
5
+ Part of **[skein-js](../../README.md)** — a TypeScript [Agent Protocol](https://github.com/langchain-ai/agent-protocol) server for [LangGraph.js](https://github.com/langchain-ai/langgraphjs), and a drop-in replacement for the LangGraph CLI.
6
6
 
7
7
  **Status:** 🚧 Pre-alpha — implemented; the v1 framework adapter.
8
8
 
@@ -19,11 +19,11 @@ out. It adds **no protocol logic of its own** — it's a thin transport shim. It
19
19
  ## Install
20
20
 
21
21
  ```bash
22
- pnpm add @skein-js/express
22
+ pnpm add @skein-js/express @langchain/langgraph
23
23
  ```
24
24
 
25
- Peer dependencies (install both in your project): **`express`** (`>=4.18`) and
26
- **`@langchain/langgraph`**.
25
+ **`express`** (`>=4.18`) and **`@langchain/langgraph`** are peer dependencies. `express` is almost
26
+ always already in your app; add it too if not (`pnpm add express`).
27
27
 
28
28
  ## Usage
29
29
 
@@ -52,10 +52,19 @@ app.listen(2024);
52
52
  ```
53
53
 
54
54
  Bring your own persistent drivers (e.g. Postgres + Redis, assembled by
55
- [`@skein-js/runtime`](../runtime)) through the same seam:
55
+ [`@skein-js/runtime`](../runtime)) through the same `deps` seam:
56
56
 
57
57
  ```ts
58
- const { router } = await skeinRouter({ deps: myProtocolDeps });
58
+ import { skeinRouter } from "@skein-js/express";
59
+ import { buildRuntime } from "@skein-js/runtime";
60
+
61
+ const runtime = await buildRuntime({
62
+ configPath: "./langgraph.json",
63
+ store: "postgres",
64
+ queue: "redis",
65
+ });
66
+ const { router } = await skeinRouter({ deps: runtime.deps, cors: runtime.cors });
67
+ app.use(router);
59
68
  ```
60
69
 
61
70
  ## API
@@ -100,7 +109,7 @@ its own.
100
109
  ## Learn more
101
110
 
102
111
  - [Agent Protocol surface](../../docs/agent-protocol.md) · [Streaming (SSE)](../../docs/streaming.md) · [React SDK / `useStream`](../../docs/react-sdk.md)
103
- - [skein-js overview](../../docs/index.md) · [Reuse-first architecture](../../docs/reuse.md)
112
+ - [skein-js overview](../../docs/index.md) · [Reuse-first architecture](../../docs/reuse.md) · [Root README](../../README.md)
104
113
 
105
114
  ## License
106
115
 
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
@@ -352,11 +352,14 @@ import express2 from "express";
352
352
  function requestLogger(logger) {
353
353
  return (req, res, next) => {
354
354
  const startedAt = Date.now();
355
+ logger.info(`<-- ${req.method} ${req.originalUrl}`);
355
356
  let logged = false;
356
357
  const log = () => {
357
358
  if (logged) return;
358
359
  logged = true;
359
- logger.info(`${req.method} ${req.originalUrl} ${res.statusCode} ${Date.now() - startedAt}ms`);
360
+ logger.info(
361
+ `--> ${req.method} ${req.originalUrl} ${res.statusCode} ${Date.now() - startedAt}ms`
362
+ );
360
363
  };
361
364
  res.once("finish", log);
362
365
  res.once("close", log);
@@ -367,6 +370,9 @@ async function createExpressServer(options) {
367
370
  const { router, runtime } = await skeinRouter(options);
368
371
  const app = express2();
369
372
  if (options.logger) app.use(requestLogger(options.logger));
373
+ app.get("/ok", (_req, res) => {
374
+ res.json({ ok: true });
375
+ });
370
376
  app.use(router);
371
377
  let server;
372
378
  return {
@@ -389,12 +395,262 @@ async function createExpressServer(options) {
389
395
  }
390
396
  };
391
397
  }
398
+
399
+ // src/langgraph-import.ts
400
+ import { readFile } from "fs/promises";
401
+ import path from "path";
402
+ import {
403
+ MemorySaver as MemorySaver2
404
+ } from "@langchain/langgraph";
405
+ import {
406
+ isTerminalRunStatus
407
+ } from "@skein-js/core";
408
+ import { SuperJSON } from "superjson";
409
+ import { z } from "zod";
410
+ var OPS_FILE = ".langgraphjs_ops.json";
411
+ var STORE_FILE = ".langgraphjs_api.store.json";
412
+ var CHECKPOINTER_FILE = ".langgraphjs_api.checkpointer.json";
413
+ var langgraphSuperjson = new SuperJSON();
414
+ langgraphSuperjson.registerCustom(
415
+ {
416
+ isApplicable: (value) => value instanceof Uint8Array,
417
+ serialize: (value) => Buffer.from(value).toString("base64"),
418
+ deserialize: (value) => new Uint8Array(Buffer.from(value, "base64"))
419
+ },
420
+ "Uint8Array"
421
+ );
422
+ var timestamp = z.union([z.date(), z.string()]).optional();
423
+ var jsonObject = z.record(z.unknown());
424
+ var langgraphAssistantSchema = z.object({
425
+ assistant_id: z.string(),
426
+ graph_id: z.string(),
427
+ name: z.string().optional(),
428
+ description: z.string().nullish(),
429
+ config: jsonObject.optional(),
430
+ context: z.unknown().optional(),
431
+ metadata: jsonObject.optional(),
432
+ version: z.number().optional(),
433
+ created_at: timestamp,
434
+ updated_at: timestamp
435
+ }).passthrough();
436
+ var langgraphThreadSchema = z.object({
437
+ thread_id: z.string(),
438
+ status: z.string().optional(),
439
+ metadata: jsonObject.optional(),
440
+ values: jsonObject.optional(),
441
+ interrupts: jsonObject.optional(),
442
+ created_at: timestamp,
443
+ updated_at: timestamp
444
+ }).passthrough();
445
+ var langgraphRunSchema = z.object({
446
+ run_id: z.string(),
447
+ thread_id: z.string(),
448
+ assistant_id: z.string(),
449
+ status: z.string().optional(),
450
+ metadata: jsonObject.optional(),
451
+ multitask_strategy: z.string().nullish(),
452
+ kwargs: jsonObject.optional(),
453
+ created_at: timestamp,
454
+ updated_at: timestamp
455
+ }).passthrough();
456
+ var langgraphOpsSchema = z.object({
457
+ assistants: z.record(langgraphAssistantSchema).optional(),
458
+ threads: z.record(langgraphThreadSchema).optional(),
459
+ runs: z.record(langgraphRunSchema).optional()
460
+ }).passthrough();
461
+ var langgraphStoreItemSchema = z.object({
462
+ namespace: z.array(z.string()),
463
+ key: z.string(),
464
+ value: jsonObject,
465
+ createdAt: timestamp,
466
+ updatedAt: timestamp
467
+ }).passthrough();
468
+ var langgraphStoreFileSchema = z.object({ data: z.map(z.string(), z.map(z.string(), langgraphStoreItemSchema)).optional() }).passthrough();
469
+ var langgraphCheckpointerSchema = z.object({ storage: jsonObject, writes: jsonObject }).passthrough();
470
+ async function readSuperjsonFile(filepath) {
471
+ let text;
472
+ try {
473
+ text = await readFile(filepath, "utf8");
474
+ } catch {
475
+ return null;
476
+ }
477
+ try {
478
+ return langgraphSuperjson.parse(text) ?? null;
479
+ } catch (error) {
480
+ throw new Error(
481
+ `Could not parse LangGraph state file "${filepath}": ${error instanceof Error ? error.message : String(error)}`
482
+ );
483
+ }
484
+ }
485
+ function validateShape(schema, value, label) {
486
+ const result = schema.safeParse(value);
487
+ if (!result.success) {
488
+ const detail = result.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
489
+ throw new Error(`Invalid LangGraph ${label}: ${detail}`);
490
+ }
491
+ return result.data;
492
+ }
493
+ function toIso(value, fallback) {
494
+ if (value instanceof Date) return value.toISOString();
495
+ if (typeof value === "string") return value;
496
+ return fallback;
497
+ }
498
+ function toAssistant(row, now) {
499
+ return {
500
+ assistant_id: row.assistant_id,
501
+ graph_id: row.graph_id,
502
+ name: row.name ?? row.graph_id,
503
+ description: row.description ?? void 0,
504
+ config: row.config ?? {},
505
+ context: row.context ?? {},
506
+ metadata: row.metadata ?? {},
507
+ version: row.version ?? 1,
508
+ created_at: toIso(row.created_at, now),
509
+ updated_at: toIso(row.updated_at, now)
510
+ };
511
+ }
512
+ function toThread(row, now) {
513
+ const createdAt = toIso(row.created_at, now);
514
+ const updatedAt = toIso(row.updated_at, createdAt);
515
+ return {
516
+ thread_id: row.thread_id,
517
+ status: row.status ?? "idle",
518
+ metadata: row.metadata ?? {},
519
+ values: row.values ?? {},
520
+ interrupts: row.interrupts ?? {},
521
+ created_at: createdAt,
522
+ updated_at: updatedAt,
523
+ state_updated_at: updatedAt
524
+ };
525
+ }
526
+ function toRun(row, now) {
527
+ const createdAt = toIso(row.created_at, now);
528
+ const status = row.status ?? "success";
529
+ return {
530
+ run_id: row.run_id,
531
+ thread_id: row.thread_id,
532
+ assistant_id: row.assistant_id,
533
+ status: isTerminalRunStatus(status) ? status : "error",
534
+ metadata: row.metadata ?? {},
535
+ multitask_strategy: row.multitask_strategy ?? null,
536
+ created_at: createdAt,
537
+ updated_at: toIso(row.updated_at, createdAt)
538
+ };
539
+ }
540
+ function toRunKwargs(kwargs) {
541
+ if (!kwargs) return {};
542
+ const { input, command, config, context, stream_mode, interrupt_before, interrupt_after } = kwargs;
543
+ return { input, command, config, context, stream_mode, interrupt_before, interrupt_after };
544
+ }
545
+ async function readLanggraphDevState(langgraphApiDir) {
546
+ const now = (/* @__PURE__ */ new Date()).toISOString();
547
+ const [opsRaw, storeRaw, checkpointerRaw] = await Promise.all([
548
+ readSuperjsonFile(path.join(langgraphApiDir, OPS_FILE)),
549
+ readSuperjsonFile(path.join(langgraphApiDir, STORE_FILE)),
550
+ readSuperjsonFile(path.join(langgraphApiDir, CHECKPOINTER_FILE))
551
+ ]);
552
+ if (!opsRaw && !storeRaw && !checkpointerRaw) return null;
553
+ const ops = opsRaw ? validateShape(langgraphOpsSchema, opsRaw, OPS_FILE) : null;
554
+ const storeFile = storeRaw ? validateShape(langgraphStoreFileSchema, storeRaw, STORE_FILE) : null;
555
+ const checkpointerFile = checkpointerRaw ? validateShape(langgraphCheckpointerSchema, checkpointerRaw, CHECKPOINTER_FILE) : null;
556
+ const runEntries = Object.entries(ops?.runs ?? {});
557
+ const items = [];
558
+ if (storeFile?.data) {
559
+ for (const namespaceItems of storeFile.data.values()) {
560
+ for (const stored of namespaceItems.values()) {
561
+ const item = {
562
+ namespace: stored.namespace,
563
+ key: stored.key,
564
+ value: stored.value,
565
+ createdAt: toIso(stored.createdAt, now),
566
+ updatedAt: toIso(stored.updatedAt, now)
567
+ };
568
+ items.push([JSON.stringify([stored.namespace, stored.key]), item]);
569
+ }
570
+ }
571
+ }
572
+ const store = {
573
+ assistants: Object.entries(ops?.assistants ?? {}).map(([id, row]) => [
574
+ id,
575
+ toAssistant(row, now)
576
+ ]),
577
+ threads: Object.entries(ops?.threads ?? {}).map(([id, row]) => [id, toThread(row, now)]),
578
+ runs: runEntries.map(([id, row]) => [id, toRun(row, now)]),
579
+ runKwargs: runEntries.map(([id, row]) => [id, toRunKwargs(row.kwargs)]),
580
+ items
581
+ };
582
+ let checkpoints = { storage: {}, writes: {} };
583
+ if (checkpointerFile) {
584
+ const saver = new MemorySaver2();
585
+ saver.storage = checkpointerFile.storage;
586
+ saver.writes = checkpointerFile.writes;
587
+ checkpoints = snapshotCheckpointer(saver);
588
+ }
589
+ return { version: 1, store, checkpoints };
590
+ }
591
+ function describeSnapshot(snapshot) {
592
+ return {
593
+ assistants: snapshot.store.assistants.length,
594
+ threads: snapshot.store.threads.length,
595
+ runs: snapshot.store.runs.length,
596
+ items: snapshot.store.items.length,
597
+ checkpointedThreads: Object.keys(snapshot.checkpoints.storage).length
598
+ };
599
+ }
600
+ function supportsRestore(store) {
601
+ return typeof store.restore === "function";
602
+ }
603
+ async function copyCheckpoints(snapshot, target) {
604
+ const source = new MemorySaver2();
605
+ hydrateCheckpointer(source, snapshot);
606
+ const tuples = [];
607
+ for await (const tuple of source.list({})) tuples.push(tuple);
608
+ tuples.reverse();
609
+ for (const tuple of tuples) {
610
+ const configurable = tuple.config.configurable ?? {};
611
+ const putConfig = {
612
+ configurable: {
613
+ thread_id: configurable.thread_id,
614
+ checkpoint_ns: configurable.checkpoint_ns ?? "",
615
+ // `put` reads `checkpoint_id` as the PARENT pointer; the child's own id is `checkpoint.id`.
616
+ checkpoint_id: tuple.parentConfig?.configurable?.checkpoint_id
617
+ }
618
+ };
619
+ const stored = await target.put(
620
+ putConfig,
621
+ tuple.checkpoint,
622
+ tuple.metadata ?? {},
623
+ tuple.checkpoint.channel_versions ?? {}
624
+ );
625
+ const writesByTask = /* @__PURE__ */ new Map();
626
+ for (const [taskId, channel, value] of tuple.pendingWrites ?? []) {
627
+ const writes = writesByTask.get(taskId) ?? [];
628
+ writes.push([channel, value]);
629
+ writesByTask.set(taskId, writes);
630
+ }
631
+ for (const [taskId, writes] of writesByTask) {
632
+ await target.putWrites(stored, writes, taskId);
633
+ }
634
+ }
635
+ }
636
+ async function loadSnapshotIntoStore(snapshot, store, checkpointer) {
637
+ if (!supportsRestore(store)) {
638
+ throw new Error(
639
+ "Target store does not support bulk import (no restore() method). Use the in-memory or Postgres driver, or implement restore() on your SkeinStore."
640
+ );
641
+ }
642
+ await store.restore(snapshot.store);
643
+ await copyCheckpoints(snapshot.checkpoints, checkpointer);
644
+ }
392
645
  export {
393
646
  corsFromHttpConfig,
394
647
  createExpressServer,
395
648
  createHandlerRouter,
649
+ describeSnapshot,
396
650
  loadInMemoryRuntime,
397
651
  loadReloadableInMemoryRuntime,
652
+ loadSnapshotIntoStore,
653
+ readLanggraphDevState,
398
654
  sendErrorResponse,
399
655
  sendProtocolResponse,
400
656
  skeinRouter,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skein-js/express",
3
- "version": "0.2.0",
3
+ "version": "0.3.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/config": "0.2.0",
45
- "@skein-js/core": "0.2.0",
46
- "@skein-js/storage-memory": "0.2.0",
47
- "@skein-js/agent-protocol": "0.2.0"
44
+ "superjson": "^2.2.6",
45
+ "zod": "^3.25.76",
46
+ "@skein-js/config": "0.3.0",
47
+ "@skein-js/agent-protocol": "0.3.0",
48
+ "@skein-js/storage-memory": "0.3.0",
49
+ "@skein-js/core": "0.3.0"
48
50
  },
49
51
  "peerDependencies": {
50
52
  "@langchain/langgraph": "^1.4.0",