@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 +27 -1
- package/dist/index.js +261 -2
- package/package.json +7 -5
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(
|
|
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(
|
|
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.
|
|
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
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"@skein-js/
|
|
47
|
-
"@skein-js/config": "0.
|
|
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",
|