@skein-js/express 0.2.1 → 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/dist/index.d.ts +27 -1
- package/dist/index.js +257 -1
- 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
|
@@ -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(
|
|
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.
|
|
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
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"@skein-js/
|
|
47
|
-
"@skein-js/
|
|
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",
|