@skein-js/express 0.5.0 → 0.7.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 +8 -139
- package/dist/index.js +19 -481
- package/package.json +10 -11
package/dist/index.d.ts
CHANGED
|
@@ -1,45 +1,13 @@
|
|
|
1
|
-
import { ProtocolRuntime, Logger,
|
|
2
|
-
|
|
3
|
-
import {
|
|
1
|
+
import { ProtocolRuntime, Logger, ProtocolHandlers, ProtocolRequest, ProtocolResponse } from '@skein-js/agent-protocol';
|
|
2
|
+
export { skeinRoutes } from '@skein-js/agent-protocol';
|
|
3
|
+
import { SkeinRuntimeOptions } from '@skein-js/server-kit';
|
|
4
|
+
export { DevStateCounts, DevStateSnapshot, InMemoryRuntimeConfig, LanggraphCorsConfig, ReloadableInMemoryRuntime, corsFromHttpConfig, describeSnapshot, loadInMemoryRuntime, loadReloadableInMemoryRuntime, loadSnapshotIntoStore, readLanggraphDevState, toCorsOptions } from '@skein-js/server-kit';
|
|
4
5
|
import { Router, Express, Request, Response } from 'express';
|
|
5
6
|
import { Server } from 'node:http';
|
|
6
|
-
import {
|
|
7
|
-
import { BaseCheckpointSaver } from '@langchain/langgraph';
|
|
8
|
-
import { SkeinStore } from '@skein-js/core';
|
|
7
|
+
import { CorsOptions } from 'cors';
|
|
9
8
|
|
|
10
|
-
interface SkeinRouterCommonOptions {
|
|
11
|
-
logger?: Logger;
|
|
12
|
-
/**
|
|
13
|
-
* Cross-origin access for browser clients (Agent Chat UI, React `useStream`). When omitted, CORS
|
|
14
|
-
* is driven by the config's `http.cors` block (LangGraph-compatible) and is otherwise **off** — we
|
|
15
|
-
* do not default to LangGraph's permissive `origin: "*"`. Set this to override: `CorsOptions` to
|
|
16
|
-
* restrict origins for `skein up`, `true` for permissive dev (reflect the request origin), or
|
|
17
|
-
* `false` to force it off. An explicit value wins over `http.cors`.
|
|
18
|
-
*/
|
|
19
|
-
cors?: boolean | CorsOptions;
|
|
20
|
-
/**
|
|
21
|
-
* Eager-load every declared graph at boot instead of lazily on first request. `skein dev` sets
|
|
22
|
-
* this so (a) graph import errors surface at startup and (b) the graph source files enter the
|
|
23
|
-
* running process's module graph — which is what arms `tsx watch`'s hot reload (it only watches
|
|
24
|
-
* files that have actually been imported). Load failures are logged, not thrown, so one bad
|
|
25
|
-
* graph never takes the server down or breaks the watch loop.
|
|
26
|
-
*/
|
|
27
|
-
warm?: boolean;
|
|
28
|
-
}
|
|
29
9
|
/** Either point at a `langgraph.json` (in-memory runtime) or inject a ready `ProtocolDeps`. */
|
|
30
|
-
type SkeinRouterOptions =
|
|
31
|
-
config: string;
|
|
32
|
-
/**
|
|
33
|
-
* How graph modules are imported for the in-memory runtime. Defaults to a native
|
|
34
|
-
* dynamic `import()`; `skein dev` injects a vite-backed importer for TypeScript graphs.
|
|
35
|
-
*/
|
|
36
|
-
importModule?: ModuleImporter;
|
|
37
|
-
deps?: never;
|
|
38
|
-
} | {
|
|
39
|
-
deps: ProtocolDeps;
|
|
40
|
-
config?: never;
|
|
41
|
-
importModule?: never;
|
|
42
|
-
});
|
|
10
|
+
type SkeinRouterOptions = SkeinRuntimeOptions;
|
|
43
11
|
interface SkeinRouter {
|
|
44
12
|
/** Mount on an Express app: `app.use(router)`. */
|
|
45
13
|
router: Router;
|
|
@@ -52,6 +20,7 @@ interface SkeinRouter {
|
|
|
52
20
|
*/
|
|
53
21
|
declare function skeinRouter(options: SkeinRouterOptions): Promise<SkeinRouter>;
|
|
54
22
|
|
|
23
|
+
/** The result of {@link createExpressServer}: the Express app, the wired runtime, and lifecycle helpers. */
|
|
55
24
|
interface SkeinExpressServer {
|
|
56
25
|
/** The Express app, protocol mounted at `/`. Mount extra middleware or routes before `listen`. */
|
|
57
26
|
app: Express;
|
|
@@ -65,21 +34,6 @@ interface SkeinExpressServer {
|
|
|
65
34
|
/** Build an Express server hosting the Agent Protocol, ready to `listen`. */
|
|
66
35
|
declare function createExpressServer(options: SkeinRouterOptions): Promise<SkeinExpressServer>;
|
|
67
36
|
|
|
68
|
-
type HttpMethod = "get" | "post" | "put" | "patch" | "delete";
|
|
69
|
-
interface RouteBinding {
|
|
70
|
-
method: HttpMethod;
|
|
71
|
-
path: string;
|
|
72
|
-
handler: keyof ProtocolHandlers;
|
|
73
|
-
/**
|
|
74
|
-
* Fold the path `thread_id` into the request body before dispatch. The SDK addresses a
|
|
75
|
-
* thread-scoped run by its path (`POST /threads/{id}/runs/stream`) while carrying only
|
|
76
|
-
* `assistant_id` in the body, but the stateless run handlers read `thread_id` from the body — so a
|
|
77
|
-
* thread-scoped mount must copy it across, or the run would target a stray new thread.
|
|
78
|
-
*/
|
|
79
|
-
foldThreadIdIntoBody?: boolean;
|
|
80
|
-
}
|
|
81
|
-
/** The route table, ordered most-specific-first within each method so literals win over params. */
|
|
82
|
-
declare const skeinRoutes: readonly RouteBinding[];
|
|
83
37
|
interface HandlerRouterOptions {
|
|
84
38
|
/** Structured logger for unexpected (non-`SkeinHttpError`) faults. */
|
|
85
39
|
logger?: Logger;
|
|
@@ -106,89 +60,4 @@ declare function sendProtocolResponse(response: ProtocolResponse, res: Response)
|
|
|
106
60
|
/** Serialize a caught error onto `res`, using the protocol status when the error carries one. */
|
|
107
61
|
declare function sendErrorResponse(error: unknown, res: Response, logger?: Logger): void;
|
|
108
62
|
|
|
109
|
-
|
|
110
|
-
type SerializedCheckpointTuple = [string, string, string | undefined];
|
|
111
|
-
/** `MemorySaver.storage` (thread → namespace → checkpointId → tuple) with blobs base64-encoded. */
|
|
112
|
-
type SerializedCheckpointStorage = Record<string, Record<string, Record<string, SerializedCheckpointTuple>>>;
|
|
113
|
-
/** `MemorySaver.writes` (key → taskId+idx → `[taskId, channel, blob]`) with the blob base64-encoded. */
|
|
114
|
-
type SerializedCheckpointWrites = Record<string, Record<string, [string, string, string]>>;
|
|
115
|
-
interface CheckpointerSnapshot {
|
|
116
|
-
storage: SerializedCheckpointStorage;
|
|
117
|
-
writes: SerializedCheckpointWrites;
|
|
118
|
-
}
|
|
119
|
-
/** A JSON-serializable snapshot of the whole in-memory dev runtime. */
|
|
120
|
-
interface DevStateSnapshot {
|
|
121
|
-
version: 1;
|
|
122
|
-
store: MemoryStoreSnapshot;
|
|
123
|
-
checkpoints: CheckpointerSnapshot;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
interface InMemoryRuntimeConfig {
|
|
127
|
-
/** In-memory `ProtocolDeps` (store, queue, bus, checkpointer) around the config's graphs. */
|
|
128
|
-
deps: ProtocolDeps;
|
|
129
|
-
/** CORS mapped from the config's `http.cors`, or `undefined` when none is declared. */
|
|
130
|
-
cors?: CorsOptions;
|
|
131
|
-
}
|
|
132
|
-
/** Load `langgraph.json`, wiring fresh in-memory drivers and reading its `http.cors` for the adapter. */
|
|
133
|
-
declare function loadInMemoryRuntime(configPath: string, importModule?: ModuleImporter, staticSchemas?: Record<string, GraphSchemas>): Promise<InMemoryRuntimeConfig>;
|
|
134
|
-
interface ReloadableInMemoryRuntime extends InMemoryRuntimeConfig {
|
|
135
|
-
/**
|
|
136
|
-
* Re-read the config and swap in freshly imported graphs, keeping the same drivers. Because the
|
|
137
|
-
* run engine calls `graphs.load()` per run (it never caches the compiled graph itself), the next
|
|
138
|
-
* run picks up the new code while every thread, run, checkpoint, and store item survives. This is
|
|
139
|
-
* what lets `skein dev` hot-reload graph source without dropping in-memory state.
|
|
140
|
-
*/
|
|
141
|
-
reloadGraphs(): Promise<void>;
|
|
142
|
-
/** A JSON-serializable snapshot of all dev state (protocol store + checkpoints). */
|
|
143
|
-
snapshotState(): DevStateSnapshot;
|
|
144
|
-
/** Restore dev state from a {@link snapshotState} — call before the server starts serving. */
|
|
145
|
-
hydrateState(snapshot: DevStateSnapshot): void;
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Like {@link loadInMemoryRuntime}, but the returned `deps.graphs` delegates to a swappable config
|
|
149
|
-
* registry so graphs can be reloaded in place, and it can snapshot/restore its dev state. `skein
|
|
150
|
-
* dev` pairs this with vite's watcher (clear vite's cache, then `reloadGraphs()` — no server
|
|
151
|
-
* restart, no lost state) and with on-disk JSON persistence across restarts.
|
|
152
|
-
*/
|
|
153
|
-
declare function loadReloadableInMemoryRuntime(configPath: string, importModule?: ModuleImporter, staticSchemas?: Record<string, GraphSchemas>): Promise<ReloadableInMemoryRuntime>;
|
|
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
|
-
|
|
179
|
-
/** The `http.cors` block of a langgraph.json — LangGraph's field names (snake_case). */
|
|
180
|
-
interface LanggraphCorsConfig {
|
|
181
|
-
allow_origins?: string[];
|
|
182
|
-
allow_origin_regex?: string;
|
|
183
|
-
allow_methods?: string[];
|
|
184
|
-
allow_headers?: string[];
|
|
185
|
-
allow_credentials?: boolean;
|
|
186
|
-
expose_headers?: string[];
|
|
187
|
-
max_age?: number;
|
|
188
|
-
}
|
|
189
|
-
/** Translate a LangGraph `http.cors` config into `cors` middleware options. */
|
|
190
|
-
declare function toCorsOptions(config: LanggraphCorsConfig): CorsOptions;
|
|
191
|
-
/** Read `http.cors` from a langgraph.json `http` block, mapped to `CorsOptions`, or `undefined`. */
|
|
192
|
-
declare function corsFromHttpConfig(http: unknown): CorsOptions | undefined;
|
|
193
|
-
|
|
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 };
|
|
63
|
+
export { type HandlerRouterOptions, type SkeinExpressServer, type SkeinRouter, type SkeinRouterOptions, createExpressServer, createHandlerRouter, sendErrorResponse, sendProtocolResponse, skeinRouter, toProtocolRequest };
|
package/dist/index.js
CHANGED
|
@@ -1,165 +1,11 @@
|
|
|
1
1
|
// src/skein-router.ts
|
|
2
|
-
import {
|
|
3
|
-
createProtocolRuntime
|
|
4
|
-
} from "@skein-js/agent-protocol";
|
|
5
|
-
|
|
6
|
-
// src/in-memory-runtime.ts
|
|
7
|
-
import { MemorySaver } from "@langchain/langgraph";
|
|
8
|
-
import {
|
|
9
|
-
loadAuthEngine,
|
|
10
|
-
loadConfig
|
|
11
|
-
} from "@skein-js/config";
|
|
12
|
-
import { MemoryRunEventBus, MemoryRunQueue, MemorySkeinStore } from "@skein-js/storage-memory";
|
|
13
|
-
|
|
14
|
-
// src/cors-config.ts
|
|
15
|
-
var ALWAYS_EXPOSED_HEADERS = ["content-location", "x-pagination-total"];
|
|
16
|
-
function toCorsOptions(config) {
|
|
17
|
-
const options = {};
|
|
18
|
-
const allowAll = config.allow_origins?.includes("*") ?? false;
|
|
19
|
-
if (config.allow_origin_regex !== void 0) {
|
|
20
|
-
const pattern = new RegExp(`^(?:${config.allow_origin_regex})$`);
|
|
21
|
-
const listed = allowAll ? void 0 : new Set(config.allow_origins);
|
|
22
|
-
options.origin = (origin, callback) => callback(
|
|
23
|
-
null,
|
|
24
|
-
origin !== void 0 && (allowAll || pattern.test(origin) || (listed?.has(origin) ?? false))
|
|
25
|
-
);
|
|
26
|
-
} else if (config.allow_origins !== void 0) {
|
|
27
|
-
options.origin = allowAll ? "*" : config.allow_origins;
|
|
28
|
-
}
|
|
29
|
-
if (config.allow_methods !== void 0) options.methods = config.allow_methods;
|
|
30
|
-
if (config.allow_headers !== void 0) options.allowedHeaders = config.allow_headers;
|
|
31
|
-
if (config.allow_credentials !== void 0) options.credentials = config.allow_credentials;
|
|
32
|
-
if (config.max_age !== void 0) options.maxAge = config.max_age;
|
|
33
|
-
const exposed = /* @__PURE__ */ new Set([...ALWAYS_EXPOSED_HEADERS, ...config.expose_headers ?? []]);
|
|
34
|
-
options.exposedHeaders = [...exposed];
|
|
35
|
-
return options;
|
|
36
|
-
}
|
|
37
|
-
function corsFromHttpConfig(http) {
|
|
38
|
-
if (typeof http !== "object" || http === null) return void 0;
|
|
39
|
-
const cors2 = http.cors;
|
|
40
|
-
if (typeof cors2 !== "object" || cors2 === null) return void 0;
|
|
41
|
-
return toCorsOptions(cors2);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// src/dev-persistence.ts
|
|
45
|
-
var toBase64 = (bytes) => Buffer.from(bytes).toString("base64");
|
|
46
|
-
var fromBase64 = (text) => new Uint8Array(Buffer.from(text, "base64"));
|
|
47
|
-
var mapValues = (record, fn) => Object.fromEntries(Object.entries(record).map(([key, value]) => [key, fn(value)]));
|
|
48
|
-
function snapshotCheckpointer(saver) {
|
|
49
|
-
return {
|
|
50
|
-
storage: mapValues(
|
|
51
|
-
saver.storage,
|
|
52
|
-
(namespaces) => mapValues(
|
|
53
|
-
namespaces,
|
|
54
|
-
(checkpoints) => mapValues(checkpoints, ([checkpoint, metadata, parentId]) => [
|
|
55
|
-
toBase64(checkpoint),
|
|
56
|
-
toBase64(metadata),
|
|
57
|
-
parentId
|
|
58
|
-
])
|
|
59
|
-
)
|
|
60
|
-
),
|
|
61
|
-
writes: mapValues(
|
|
62
|
-
saver.writes,
|
|
63
|
-
(taskWrites) => mapValues(taskWrites, ([taskId, channel, blob]) => [
|
|
64
|
-
taskId,
|
|
65
|
-
channel,
|
|
66
|
-
toBase64(blob)
|
|
67
|
-
])
|
|
68
|
-
)
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
function hydrateCheckpointer(saver, snapshot) {
|
|
72
|
-
saver.storage = mapValues(
|
|
73
|
-
snapshot.storage,
|
|
74
|
-
(namespaces) => mapValues(
|
|
75
|
-
namespaces,
|
|
76
|
-
(checkpoints) => mapValues(
|
|
77
|
-
checkpoints,
|
|
78
|
-
([checkpoint, metadata, parentId]) => [
|
|
79
|
-
fromBase64(checkpoint),
|
|
80
|
-
fromBase64(metadata),
|
|
81
|
-
parentId
|
|
82
|
-
]
|
|
83
|
-
)
|
|
84
|
-
)
|
|
85
|
-
);
|
|
86
|
-
saver.writes = mapValues(
|
|
87
|
-
snapshot.writes,
|
|
88
|
-
(taskWrites) => mapValues(taskWrites, ([taskId, channel, blob]) => [
|
|
89
|
-
taskId,
|
|
90
|
-
channel,
|
|
91
|
-
fromBase64(blob)
|
|
92
|
-
])
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// src/in-memory-runtime.ts
|
|
97
|
-
function toGraphResolver(graphs) {
|
|
98
|
-
return {
|
|
99
|
-
ids: graphs.ids,
|
|
100
|
-
load: (graphId) => graphs.load(graphId),
|
|
101
|
-
schemas: async (graphId) => await graphs.schemas(graphId)
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
function buildInMemoryDeps(graphs) {
|
|
105
|
-
return {
|
|
106
|
-
store: new MemorySkeinStore(),
|
|
107
|
-
graphs,
|
|
108
|
-
queue: new MemoryRunQueue(),
|
|
109
|
-
bus: new MemoryRunEventBus(),
|
|
110
|
-
checkpointer: new MemorySaver()
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
async function loadInMemoryRuntime(configPath, importModule, staticSchemas) {
|
|
114
|
-
const { graphs, config, configDir } = await loadConfig({
|
|
115
|
-
configPath,
|
|
116
|
-
importModule,
|
|
117
|
-
staticSchemas
|
|
118
|
-
});
|
|
119
|
-
const deps = buildInMemoryDeps(toGraphResolver(graphs));
|
|
120
|
-
deps.auth = await loadAuthEngine(config.auth, { configDir, importModule });
|
|
121
|
-
return {
|
|
122
|
-
deps,
|
|
123
|
-
cors: corsFromHttpConfig(config.http)
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
async function loadReloadableInMemoryRuntime(configPath, importModule, staticSchemas) {
|
|
127
|
-
const first = await loadConfig({ configPath, importModule, staticSchemas });
|
|
128
|
-
let current = first.graphs;
|
|
129
|
-
const graphs = {
|
|
130
|
-
ids: first.graphs.ids,
|
|
131
|
-
load: (graphId) => current.load(graphId),
|
|
132
|
-
schemas: async (graphId) => await current.schemas(graphId)
|
|
133
|
-
};
|
|
134
|
-
const store = new MemorySkeinStore();
|
|
135
|
-
const checkpointer = new MemorySaver();
|
|
136
|
-
const deps = {
|
|
137
|
-
store,
|
|
138
|
-
graphs,
|
|
139
|
-
queue: new MemoryRunQueue(),
|
|
140
|
-
bus: new MemoryRunEventBus(),
|
|
141
|
-
checkpointer,
|
|
142
|
-
auth: await loadAuthEngine(first.config.auth, { configDir: first.configDir, importModule })
|
|
143
|
-
};
|
|
144
|
-
return {
|
|
145
|
-
deps,
|
|
146
|
-
cors: corsFromHttpConfig(first.config.http),
|
|
147
|
-
reloadGraphs: async () => {
|
|
148
|
-
current = (await loadConfig({ configPath, importModule, staticSchemas })).graphs;
|
|
149
|
-
},
|
|
150
|
-
snapshotState: () => ({
|
|
151
|
-
version: 1,
|
|
152
|
-
store: store.snapshot(),
|
|
153
|
-
checkpoints: snapshotCheckpointer(checkpointer)
|
|
154
|
-
}),
|
|
155
|
-
hydrateState: (snapshot) => {
|
|
156
|
-
store.hydrate(snapshot.store);
|
|
157
|
-
hydrateCheckpointer(checkpointer, snapshot.checkpoints);
|
|
158
|
-
}
|
|
159
|
-
};
|
|
160
|
-
}
|
|
2
|
+
import { resolveProtocolRuntime } from "@skein-js/server-kit";
|
|
161
3
|
|
|
162
4
|
// src/routes.ts
|
|
5
|
+
import {
|
|
6
|
+
copyThreadIdIntoBody,
|
|
7
|
+
skeinRoutes
|
|
8
|
+
} from "@skein-js/agent-protocol";
|
|
163
9
|
import cors from "cors";
|
|
164
10
|
import express from "express";
|
|
165
11
|
|
|
@@ -252,59 +98,7 @@ function toProtocolRequest(req) {
|
|
|
252
98
|
}
|
|
253
99
|
|
|
254
100
|
// src/routes.ts
|
|
255
|
-
|
|
256
|
-
// assistants
|
|
257
|
-
{ method: "get", path: "/assistants/:assistant_id/schemas", handler: "getAssistantSchemas" },
|
|
258
|
-
{ method: "get", path: "/assistants/:assistant_id", handler: "getAssistant" },
|
|
259
|
-
{ method: "post", path: "/assistants/search", handler: "searchAssistants" },
|
|
260
|
-
// threads
|
|
261
|
-
{ method: "post", path: "/threads/search", handler: "listThreads" },
|
|
262
|
-
{ method: "post", path: "/threads", handler: "createThread" },
|
|
263
|
-
{ method: "post", path: "/threads/:thread_id/copy", handler: "copyThread" },
|
|
264
|
-
{ method: "get", path: "/threads/:thread_id", handler: "getThread" },
|
|
265
|
-
{ method: "patch", path: "/threads/:thread_id", handler: "patchThread" },
|
|
266
|
-
{ method: "delete", path: "/threads/:thread_id", handler: "deleteThread" },
|
|
267
|
-
{ method: "get", path: "/threads/:thread_id/state", handler: "getThreadState" },
|
|
268
|
-
{ method: "post", path: "/threads/:thread_id/history", handler: "getThreadHistory" },
|
|
269
|
-
// runs — the stateless handlers are reused on the thread-scoped path with the id folded in
|
|
270
|
-
{ method: "post", path: "/runs/wait", handler: "createWaitRun" },
|
|
271
|
-
{ method: "post", path: "/runs/stream", handler: "createStreamRun" },
|
|
272
|
-
{
|
|
273
|
-
method: "post",
|
|
274
|
-
path: "/threads/:thread_id/runs/wait",
|
|
275
|
-
handler: "createWaitRun",
|
|
276
|
-
foldThreadIdIntoBody: true
|
|
277
|
-
},
|
|
278
|
-
{
|
|
279
|
-
method: "post",
|
|
280
|
-
path: "/threads/:thread_id/runs/stream",
|
|
281
|
-
handler: "createStreamRun",
|
|
282
|
-
foldThreadIdIntoBody: true
|
|
283
|
-
},
|
|
284
|
-
{ method: "post", path: "/threads/:thread_id/runs", handler: "createBackgroundRun" },
|
|
285
|
-
{ method: "get", path: "/threads/:thread_id/runs", handler: "listThreadRuns" },
|
|
286
|
-
{ method: "post", path: "/threads/:thread_id/runs/:run_id/cancel", handler: "cancelRun" },
|
|
287
|
-
{ method: "get", path: "/threads/:thread_id/runs/:run_id/stream", handler: "joinRunStream" },
|
|
288
|
-
{ method: "get", path: "/threads/:thread_id/runs/:run_id", handler: "getRun" },
|
|
289
|
-
{ method: "delete", path: "/threads/:thread_id/runs/:run_id", handler: "deleteRun" },
|
|
290
|
-
{ method: "get", path: "/runs/:run_id/stream", handler: "joinRunStream" },
|
|
291
|
-
// thread streaming / commands
|
|
292
|
-
{ method: "post", path: "/threads/:thread_id/stream", handler: "postThreadStream" },
|
|
293
|
-
{ method: "get", path: "/threads/:thread_id/stream", handler: "getThreadStream" },
|
|
294
|
-
{ method: "post", path: "/threads/:thread_id/commands", handler: "postThreadCommands" },
|
|
295
|
-
// store
|
|
296
|
-
{ method: "put", path: "/store/items", handler: "putStoreItem" },
|
|
297
|
-
{ method: "get", path: "/store/items", handler: "getStoreItem" },
|
|
298
|
-
{ method: "delete", path: "/store/items", handler: "deleteStoreItem" },
|
|
299
|
-
{ method: "post", path: "/store/items/search", handler: "searchStoreItems" },
|
|
300
|
-
{ method: "post", path: "/store/namespaces", handler: "listStoreNamespaces" }
|
|
301
|
-
];
|
|
302
|
-
function foldThreadId(request) {
|
|
303
|
-
const threadId = request.params["thread_id"];
|
|
304
|
-
if (threadId === void 0) return request;
|
|
305
|
-
const base = typeof request.body === "object" && request.body !== null && !Array.isArray(request.body) ? request.body : {};
|
|
306
|
-
return { ...request, body: { ...base, thread_id: threadId } };
|
|
307
|
-
}
|
|
101
|
+
import { skeinRoutes as skeinRoutes2 } from "@skein-js/agent-protocol";
|
|
308
102
|
function createHandlerRouter(handlers, options = {}) {
|
|
309
103
|
const router = express.Router();
|
|
310
104
|
if (options.cors) router.use(cors(options.cors === true ? { origin: true } : options.cors));
|
|
@@ -315,7 +109,7 @@ function createHandlerRouter(handlers, options = {}) {
|
|
|
315
109
|
try {
|
|
316
110
|
const request = toProtocolRequest(req);
|
|
317
111
|
const response = await invoke(
|
|
318
|
-
binding.foldThreadIdIntoBody ?
|
|
112
|
+
binding.foldThreadIdIntoBody ? copyThreadIdIntoBody(request) : request
|
|
319
113
|
);
|
|
320
114
|
await sendProtocolResponse(response, res);
|
|
321
115
|
} catch (error) {
|
|
@@ -328,31 +122,11 @@ function createHandlerRouter(handlers, options = {}) {
|
|
|
328
122
|
|
|
329
123
|
// src/skein-router.ts
|
|
330
124
|
async function skeinRouter(options) {
|
|
331
|
-
|
|
332
|
-
let corsFromConfig;
|
|
333
|
-
if (options.deps) {
|
|
334
|
-
deps = options.deps;
|
|
335
|
-
} else {
|
|
336
|
-
const loaded = await loadInMemoryRuntime(options.config, options.importModule);
|
|
337
|
-
deps = loaded.deps;
|
|
338
|
-
corsFromConfig = loaded.cors;
|
|
339
|
-
}
|
|
340
|
-
const runtime = createProtocolRuntime(deps);
|
|
341
|
-
await runtime.service.assistants.registerGraphAssistants();
|
|
342
|
-
if (options.warm) {
|
|
343
|
-
await Promise.all(
|
|
344
|
-
deps.graphs.ids.map(
|
|
345
|
-
(graphId) => deps.graphs.load(graphId).catch((error) => {
|
|
346
|
-
options.logger?.warn(`Failed to warm graph "${graphId}".`, error);
|
|
347
|
-
})
|
|
348
|
-
)
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
|
-
runtime.worker.start();
|
|
125
|
+
const { runtime, cors: cors2 } = await resolveProtocolRuntime(options);
|
|
352
126
|
const router = createHandlerRouter(runtime.handlers, {
|
|
353
127
|
logger: options.logger,
|
|
354
128
|
// Explicit option wins; otherwise fall back to the config's `http.cors`, else off.
|
|
355
|
-
cors: options.cors ??
|
|
129
|
+
cors: options.cors ?? cors2 ?? false
|
|
356
130
|
});
|
|
357
131
|
return { router, runtime };
|
|
358
132
|
}
|
|
@@ -406,252 +180,16 @@ async function createExpressServer(options) {
|
|
|
406
180
|
};
|
|
407
181
|
}
|
|
408
182
|
|
|
409
|
-
// src/
|
|
410
|
-
import { readFile } from "fs/promises";
|
|
411
|
-
import path from "path";
|
|
412
|
-
import {
|
|
413
|
-
MemorySaver as MemorySaver2
|
|
414
|
-
} from "@langchain/langgraph";
|
|
183
|
+
// src/index.ts
|
|
415
184
|
import {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
langgraphSuperjson.registerCustom(
|
|
425
|
-
{
|
|
426
|
-
isApplicable: (value) => value instanceof Uint8Array,
|
|
427
|
-
serialize: (value) => Buffer.from(value).toString("base64"),
|
|
428
|
-
deserialize: (value) => new Uint8Array(Buffer.from(value, "base64"))
|
|
429
|
-
},
|
|
430
|
-
"Uint8Array"
|
|
431
|
-
);
|
|
432
|
-
var timestamp = z.union([z.date(), z.string()]).optional();
|
|
433
|
-
var jsonObject = z.record(z.unknown());
|
|
434
|
-
var langgraphAssistantSchema = z.object({
|
|
435
|
-
assistant_id: z.string(),
|
|
436
|
-
graph_id: z.string(),
|
|
437
|
-
name: z.string().optional(),
|
|
438
|
-
description: z.string().nullish(),
|
|
439
|
-
config: jsonObject.optional(),
|
|
440
|
-
context: z.unknown().optional(),
|
|
441
|
-
metadata: jsonObject.optional(),
|
|
442
|
-
version: z.number().optional(),
|
|
443
|
-
created_at: timestamp,
|
|
444
|
-
updated_at: timestamp
|
|
445
|
-
}).passthrough();
|
|
446
|
-
var langgraphThreadSchema = z.object({
|
|
447
|
-
thread_id: z.string(),
|
|
448
|
-
status: z.string().optional(),
|
|
449
|
-
metadata: jsonObject.optional(),
|
|
450
|
-
values: jsonObject.optional(),
|
|
451
|
-
interrupts: jsonObject.optional(),
|
|
452
|
-
created_at: timestamp,
|
|
453
|
-
updated_at: timestamp
|
|
454
|
-
}).passthrough();
|
|
455
|
-
var langgraphRunSchema = z.object({
|
|
456
|
-
run_id: z.string(),
|
|
457
|
-
thread_id: z.string(),
|
|
458
|
-
assistant_id: z.string(),
|
|
459
|
-
status: z.string().optional(),
|
|
460
|
-
metadata: jsonObject.optional(),
|
|
461
|
-
multitask_strategy: z.string().nullish(),
|
|
462
|
-
kwargs: jsonObject.optional(),
|
|
463
|
-
created_at: timestamp,
|
|
464
|
-
updated_at: timestamp
|
|
465
|
-
}).passthrough();
|
|
466
|
-
var langgraphOpsSchema = z.object({
|
|
467
|
-
assistants: z.record(langgraphAssistantSchema).optional(),
|
|
468
|
-
threads: z.record(langgraphThreadSchema).optional(),
|
|
469
|
-
runs: z.record(langgraphRunSchema).optional()
|
|
470
|
-
}).passthrough();
|
|
471
|
-
var langgraphStoreItemSchema = z.object({
|
|
472
|
-
namespace: z.array(z.string()),
|
|
473
|
-
key: z.string(),
|
|
474
|
-
value: jsonObject,
|
|
475
|
-
createdAt: timestamp,
|
|
476
|
-
updatedAt: timestamp
|
|
477
|
-
}).passthrough();
|
|
478
|
-
var langgraphStoreFileSchema = z.object({ data: z.map(z.string(), z.map(z.string(), langgraphStoreItemSchema)).optional() }).passthrough();
|
|
479
|
-
var langgraphCheckpointerSchema = z.object({ storage: jsonObject, writes: jsonObject }).passthrough();
|
|
480
|
-
async function readSuperjsonFile(filepath) {
|
|
481
|
-
let text;
|
|
482
|
-
try {
|
|
483
|
-
text = await readFile(filepath, "utf8");
|
|
484
|
-
} catch {
|
|
485
|
-
return null;
|
|
486
|
-
}
|
|
487
|
-
try {
|
|
488
|
-
return langgraphSuperjson.parse(text) ?? null;
|
|
489
|
-
} catch (error) {
|
|
490
|
-
throw new Error(
|
|
491
|
-
`Could not parse LangGraph state file "${filepath}": ${error instanceof Error ? error.message : String(error)}`
|
|
492
|
-
);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
function validateShape(schema, value, label) {
|
|
496
|
-
const result = schema.safeParse(value);
|
|
497
|
-
if (!result.success) {
|
|
498
|
-
const detail = result.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
499
|
-
throw new Error(`Invalid LangGraph ${label}: ${detail}`);
|
|
500
|
-
}
|
|
501
|
-
return result.data;
|
|
502
|
-
}
|
|
503
|
-
function toIso(value, fallback) {
|
|
504
|
-
if (value instanceof Date) return value.toISOString();
|
|
505
|
-
if (typeof value === "string") return value;
|
|
506
|
-
return fallback;
|
|
507
|
-
}
|
|
508
|
-
function toAssistant(row, now) {
|
|
509
|
-
return {
|
|
510
|
-
assistant_id: row.assistant_id,
|
|
511
|
-
graph_id: row.graph_id,
|
|
512
|
-
name: row.name ?? row.graph_id,
|
|
513
|
-
description: row.description ?? void 0,
|
|
514
|
-
config: row.config ?? {},
|
|
515
|
-
context: row.context ?? {},
|
|
516
|
-
metadata: row.metadata ?? {},
|
|
517
|
-
version: row.version ?? 1,
|
|
518
|
-
created_at: toIso(row.created_at, now),
|
|
519
|
-
updated_at: toIso(row.updated_at, now)
|
|
520
|
-
};
|
|
521
|
-
}
|
|
522
|
-
function toThread(row, now) {
|
|
523
|
-
const createdAt = toIso(row.created_at, now);
|
|
524
|
-
const updatedAt = toIso(row.updated_at, createdAt);
|
|
525
|
-
return {
|
|
526
|
-
thread_id: row.thread_id,
|
|
527
|
-
status: row.status ?? "idle",
|
|
528
|
-
metadata: row.metadata ?? {},
|
|
529
|
-
values: row.values ?? {},
|
|
530
|
-
interrupts: row.interrupts ?? {},
|
|
531
|
-
created_at: createdAt,
|
|
532
|
-
updated_at: updatedAt,
|
|
533
|
-
state_updated_at: updatedAt
|
|
534
|
-
};
|
|
535
|
-
}
|
|
536
|
-
function toRun(row, now) {
|
|
537
|
-
const createdAt = toIso(row.created_at, now);
|
|
538
|
-
const status = row.status ?? "success";
|
|
539
|
-
return {
|
|
540
|
-
run_id: row.run_id,
|
|
541
|
-
thread_id: row.thread_id,
|
|
542
|
-
assistant_id: row.assistant_id,
|
|
543
|
-
status: isTerminalRunStatus(status) ? status : "error",
|
|
544
|
-
metadata: row.metadata ?? {},
|
|
545
|
-
multitask_strategy: row.multitask_strategy ?? null,
|
|
546
|
-
created_at: createdAt,
|
|
547
|
-
updated_at: toIso(row.updated_at, createdAt)
|
|
548
|
-
};
|
|
549
|
-
}
|
|
550
|
-
function toRunKwargs(kwargs) {
|
|
551
|
-
if (!kwargs) return {};
|
|
552
|
-
const { input, command, config, context, stream_mode, interrupt_before, interrupt_after } = kwargs;
|
|
553
|
-
return { input, command, config, context, stream_mode, interrupt_before, interrupt_after };
|
|
554
|
-
}
|
|
555
|
-
async function readLanggraphDevState(langgraphApiDir) {
|
|
556
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
557
|
-
const [opsRaw, storeRaw, checkpointerRaw] = await Promise.all([
|
|
558
|
-
readSuperjsonFile(path.join(langgraphApiDir, OPS_FILE)),
|
|
559
|
-
readSuperjsonFile(path.join(langgraphApiDir, STORE_FILE)),
|
|
560
|
-
readSuperjsonFile(path.join(langgraphApiDir, CHECKPOINTER_FILE))
|
|
561
|
-
]);
|
|
562
|
-
if (!opsRaw && !storeRaw && !checkpointerRaw) return null;
|
|
563
|
-
const ops = opsRaw ? validateShape(langgraphOpsSchema, opsRaw, OPS_FILE) : null;
|
|
564
|
-
const storeFile = storeRaw ? validateShape(langgraphStoreFileSchema, storeRaw, STORE_FILE) : null;
|
|
565
|
-
const checkpointerFile = checkpointerRaw ? validateShape(langgraphCheckpointerSchema, checkpointerRaw, CHECKPOINTER_FILE) : null;
|
|
566
|
-
const runEntries = Object.entries(ops?.runs ?? {});
|
|
567
|
-
const items = [];
|
|
568
|
-
if (storeFile?.data) {
|
|
569
|
-
for (const namespaceItems of storeFile.data.values()) {
|
|
570
|
-
for (const stored of namespaceItems.values()) {
|
|
571
|
-
const item = {
|
|
572
|
-
namespace: stored.namespace,
|
|
573
|
-
key: stored.key,
|
|
574
|
-
value: stored.value,
|
|
575
|
-
createdAt: toIso(stored.createdAt, now),
|
|
576
|
-
updatedAt: toIso(stored.updatedAt, now)
|
|
577
|
-
};
|
|
578
|
-
items.push([JSON.stringify([stored.namespace, stored.key]), item]);
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
const store = {
|
|
583
|
-
assistants: Object.entries(ops?.assistants ?? {}).map(([id, row]) => [
|
|
584
|
-
id,
|
|
585
|
-
toAssistant(row, now)
|
|
586
|
-
]),
|
|
587
|
-
threads: Object.entries(ops?.threads ?? {}).map(([id, row]) => [id, toThread(row, now)]),
|
|
588
|
-
runs: runEntries.map(([id, row]) => [id, toRun(row, now)]),
|
|
589
|
-
runKwargs: runEntries.map(([id, row]) => [id, toRunKwargs(row.kwargs)]),
|
|
590
|
-
items
|
|
591
|
-
};
|
|
592
|
-
let checkpoints = { storage: {}, writes: {} };
|
|
593
|
-
if (checkpointerFile) {
|
|
594
|
-
const saver = new MemorySaver2();
|
|
595
|
-
saver.storage = checkpointerFile.storage;
|
|
596
|
-
saver.writes = checkpointerFile.writes;
|
|
597
|
-
checkpoints = snapshotCheckpointer(saver);
|
|
598
|
-
}
|
|
599
|
-
return { version: 1, store, checkpoints };
|
|
600
|
-
}
|
|
601
|
-
function describeSnapshot(snapshot) {
|
|
602
|
-
return {
|
|
603
|
-
assistants: snapshot.store.assistants.length,
|
|
604
|
-
threads: snapshot.store.threads.length,
|
|
605
|
-
runs: snapshot.store.runs.length,
|
|
606
|
-
items: snapshot.store.items.length,
|
|
607
|
-
checkpointedThreads: Object.keys(snapshot.checkpoints.storage).length
|
|
608
|
-
};
|
|
609
|
-
}
|
|
610
|
-
function supportsRestore(store) {
|
|
611
|
-
return typeof store.restore === "function";
|
|
612
|
-
}
|
|
613
|
-
async function copyCheckpoints(snapshot, target) {
|
|
614
|
-
const source = new MemorySaver2();
|
|
615
|
-
hydrateCheckpointer(source, snapshot);
|
|
616
|
-
const tuples = [];
|
|
617
|
-
for await (const tuple of source.list({})) tuples.push(tuple);
|
|
618
|
-
tuples.reverse();
|
|
619
|
-
for (const tuple of tuples) {
|
|
620
|
-
const configurable = tuple.config.configurable ?? {};
|
|
621
|
-
const putConfig = {
|
|
622
|
-
configurable: {
|
|
623
|
-
thread_id: configurable.thread_id,
|
|
624
|
-
checkpoint_ns: configurable.checkpoint_ns ?? "",
|
|
625
|
-
// `put` reads `checkpoint_id` as the PARENT pointer; the child's own id is `checkpoint.id`.
|
|
626
|
-
checkpoint_id: tuple.parentConfig?.configurable?.checkpoint_id
|
|
627
|
-
}
|
|
628
|
-
};
|
|
629
|
-
const stored = await target.put(
|
|
630
|
-
putConfig,
|
|
631
|
-
tuple.checkpoint,
|
|
632
|
-
tuple.metadata ?? {},
|
|
633
|
-
tuple.checkpoint.channel_versions ?? {}
|
|
634
|
-
);
|
|
635
|
-
const writesByTask = /* @__PURE__ */ new Map();
|
|
636
|
-
for (const [taskId, channel, value] of tuple.pendingWrites ?? []) {
|
|
637
|
-
const writes = writesByTask.get(taskId) ?? [];
|
|
638
|
-
writes.push([channel, value]);
|
|
639
|
-
writesByTask.set(taskId, writes);
|
|
640
|
-
}
|
|
641
|
-
for (const [taskId, writes] of writesByTask) {
|
|
642
|
-
await target.putWrites(stored, writes, taskId);
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
async function loadSnapshotIntoStore(snapshot, store, checkpointer) {
|
|
647
|
-
if (!supportsRestore(store)) {
|
|
648
|
-
throw new Error(
|
|
649
|
-
"Target store does not support bulk import (no restore() method). Use the in-memory or Postgres driver, or implement restore() on your SkeinStore."
|
|
650
|
-
);
|
|
651
|
-
}
|
|
652
|
-
await store.restore(snapshot.store);
|
|
653
|
-
await copyCheckpoints(snapshot.checkpoints, checkpointer);
|
|
654
|
-
}
|
|
185
|
+
loadInMemoryRuntime,
|
|
186
|
+
loadReloadableInMemoryRuntime,
|
|
187
|
+
readLanggraphDevState,
|
|
188
|
+
loadSnapshotIntoStore,
|
|
189
|
+
describeSnapshot,
|
|
190
|
+
corsFromHttpConfig,
|
|
191
|
+
toCorsOptions
|
|
192
|
+
} from "@skein-js/server-kit";
|
|
655
193
|
export {
|
|
656
194
|
corsFromHttpConfig,
|
|
657
195
|
createExpressServer,
|
|
@@ -664,7 +202,7 @@ export {
|
|
|
664
202
|
sendErrorResponse,
|
|
665
203
|
sendProtocolResponse,
|
|
666
204
|
skeinRouter,
|
|
667
|
-
skeinRoutes,
|
|
205
|
+
skeinRoutes2 as skeinRoutes,
|
|
668
206
|
toCorsOptions,
|
|
669
207
|
toProtocolRequest
|
|
670
208
|
};
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skein-js/express",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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>",
|
|
7
|
-
"homepage": "https://github.com/
|
|
7
|
+
"homepage": "https://github.com/skein-js/skein-js/tree/main/packages/server-express#readme",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/
|
|
10
|
+
"url": "git+https://github.com/skein-js/skein-js.git",
|
|
11
11
|
"directory": "packages/server-express"
|
|
12
12
|
},
|
|
13
13
|
"bugs": {
|
|
14
|
-
"url": "https://github.com/
|
|
14
|
+
"url": "https://github.com/skein-js/skein-js/issues"
|
|
15
15
|
},
|
|
16
16
|
"keywords": [
|
|
17
17
|
"typescript",
|
|
@@ -41,12 +41,10 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"cors": "^2.8.5",
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"@skein-js/
|
|
47
|
-
"@skein-js/config": "0.
|
|
48
|
-
"@skein-js/agent-protocol": "0.5.0",
|
|
49
|
-
"@skein-js/storage-memory": "0.5.0"
|
|
44
|
+
"@skein-js/agent-protocol": "0.7.0",
|
|
45
|
+
"@skein-js/core": "0.7.0",
|
|
46
|
+
"@skein-js/server-kit": "0.7.0",
|
|
47
|
+
"@skein-js/config": "0.7.0"
|
|
50
48
|
},
|
|
51
49
|
"peerDependencies": {
|
|
52
50
|
"@langchain/langgraph": "^1.4.0",
|
|
@@ -57,7 +55,8 @@
|
|
|
57
55
|
"@langchain/langgraph": "^1.4.0",
|
|
58
56
|
"@types/cors": "^2.8.17",
|
|
59
57
|
"@types/express": "^5.0.0",
|
|
60
|
-
"express": "^5.2.1"
|
|
58
|
+
"express": "^5.2.1",
|
|
59
|
+
"@skein-js/storage-memory": "0.7.0"
|
|
61
60
|
},
|
|
62
61
|
"publishConfig": {
|
|
63
62
|
"access": "public"
|