@skein-js/nextjs 0.8.0 → 0.9.1
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 +20 -0
- package/dist/index.d.ts +24 -6
- package/dist/index.js +118 -27
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -50,6 +50,23 @@ export default createSkeinPagesHandler({ config: "./langgraph.json" });
|
|
|
50
50
|
Both entry points accept the shared `{ config } | { deps }` seam, an optional `cors` (off by default),
|
|
51
51
|
and a `basePath` (defaults to `/api`, matching the mount above).
|
|
52
52
|
|
|
53
|
+
## Graphs as plain endpoints (non-chat)
|
|
54
|
+
|
|
55
|
+
For workloads that aren't chat — a classifier, an extractor, a workflow another service calls — there
|
|
56
|
+
is a smaller surface: every graph mounted as `POST <basePath>/:graph_id`, where the request body **is**
|
|
57
|
+
the graph input and the response **is** the final state. No threads, assistants, or runs.
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
// app/api/invoke/[graph_id]/route.ts
|
|
61
|
+
import { createSkeinInvokeRouteHandlers } from "@skein-js/nextjs";
|
|
62
|
+
|
|
63
|
+
export const runtime = "nodejs";
|
|
64
|
+
export const { POST } = createSkeinInvokeRouteHandlers({ deps, basePath: "/api/invoke" });
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Send `Accept: text/event-stream` to stream the steps instead. See
|
|
68
|
+
[docs/serving-a-single-graph.md](../../docs/serving-a-single-graph.md).
|
|
69
|
+
|
|
53
70
|
## Deployment caveat
|
|
54
71
|
|
|
55
72
|
The background run worker and the in-memory driver need a **long-lived Node process** — fine on
|
|
@@ -63,6 +80,9 @@ The background run worker and the in-memory driver need a **long-lived Node proc
|
|
|
63
80
|
`{ GET, POST, PUT, PATCH, DELETE, OPTIONS }` to re-export from `app/<base>/[...path]/route.ts`.
|
|
64
81
|
- **`createSkeinPagesHandler(options): SkeinPagesHandler`** — Pages Router; the default export for
|
|
65
82
|
`pages/api/[...path].ts`.
|
|
83
|
+
- **`createSkeinInvokeRouteHandlers(options): SkeinInvokeRouteHandlers`** — the simplified serving
|
|
84
|
+
surface (`POST <basePath>/:graph_id`, body-in / final-state-out); returns `{ POST, OPTIONS }` for
|
|
85
|
+
`app/api/invoke/[graph_id]/route.ts`. `basePath` defaults to `/api/invoke`; adds `streamMode`.
|
|
66
86
|
- Both accept `SkeinRouteHandlerOptions` / `SkeinPagesHandlerOptions`: the shared
|
|
67
87
|
`{ config, importModule? } | { deps }` seam **plus** an optional `cors` (off by default) and a
|
|
68
88
|
`basePath` (defaults to `/api`, matching the mount). `deps` comes from
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { SkeinRuntimeOptions, ResolvedProtocolRuntime } from '@skein-js/server-kit';
|
|
2
|
-
export { SkeinRuntimeOptions, sendNodeError, sendNodeResponse } from '@skein-js/server-kit';
|
|
1
|
+
import { SkeinRuntimeOptions, ResolvedRuntimeDeps, ResolvedProtocolRuntime } from '@skein-js/server-kit';
|
|
2
|
+
export { RunWorkerOptions, SkeinRuntimeOptions, sendNodeError, sendNodeResponse } from '@skein-js/server-kit';
|
|
3
3
|
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
4
|
-
import { ProtocolResponse, Logger } from '@skein-js/agent-protocol';
|
|
4
|
+
import { GraphInvokeOptions, ProtocolResponse, Logger } from '@skein-js/agent-protocol';
|
|
5
5
|
|
|
6
6
|
/** Options for the App Router handlers: the shared runtime options plus where the catch-all is mounted. */
|
|
7
7
|
type SkeinRouteHandlerOptions = SkeinRuntimeOptions & {
|
|
@@ -40,13 +40,31 @@ type SkeinPagesHandler = (req: SkeinPagesRequest, res: ServerResponse) => Promis
|
|
|
40
40
|
/** Build the Pages Router API handler for the given options. */
|
|
41
41
|
declare function createSkeinPagesHandler(options: SkeinPagesHandlerOptions): SkeinPagesHandler;
|
|
42
42
|
|
|
43
|
-
type
|
|
43
|
+
type SkeinInvokeRouteHandlerOptions = SkeinRuntimeOptions & GraphInvokeOptions & {
|
|
44
|
+
/**
|
|
45
|
+
* The path this route is mounted at, stripped before reading the trailing `:graph_id` segment.
|
|
46
|
+
* Defaults to `/api/invoke` (i.e. `app/api/invoke/[graph_id]/route.ts`).
|
|
47
|
+
*/
|
|
48
|
+
basePath?: string;
|
|
49
|
+
};
|
|
50
|
+
interface SkeinInvokeRouteHandlers {
|
|
51
|
+
POST: (request: Request) => Promise<Response>;
|
|
52
|
+
OPTIONS: (request: Request) => Promise<Response>;
|
|
53
|
+
}
|
|
54
|
+
/** Build the App Router handlers for the invoke surface. */
|
|
55
|
+
declare function createSkeinInvokeRouteHandlers(options: SkeinInvokeRouteHandlerOptions): SkeinInvokeRouteHandlers;
|
|
56
|
+
|
|
44
57
|
/** Resolve (once) the runtime for these options, reusing the cached promise across module reloads. */
|
|
45
|
-
declare function getSkeinRuntime(options: SkeinRuntimeOptions):
|
|
58
|
+
declare function getSkeinRuntime(options: SkeinRuntimeOptions): Promise<ResolvedProtocolRuntime>;
|
|
59
|
+
/**
|
|
60
|
+
* Resolve (once) just the deps for these options — what the simplified invoke surface needs, since it
|
|
61
|
+
* seeds no assistants and starts no background run worker.
|
|
62
|
+
*/
|
|
63
|
+
declare function getSkeinInvokeDeps(options: SkeinRuntimeOptions): Promise<ResolvedRuntimeDeps>;
|
|
46
64
|
|
|
47
65
|
/** Serialize a `ProtocolResponse` into a Web `Response`, merging any `extraHeaders` (e.g. CORS). */
|
|
48
66
|
declare function toWebResponse(response: ProtocolResponse, extraHeaders?: Record<string, string>): Response;
|
|
49
67
|
/** Map a thrown error onto a Web `Response` — `SkeinHttpError` to its status, else a logged 500. */
|
|
50
68
|
declare function webErrorResponse(error: unknown, extraHeaders?: Record<string, string>, logger?: Logger): Response;
|
|
51
69
|
|
|
52
|
-
export { type SkeinPagesHandler, type SkeinPagesHandlerOptions, type SkeinPagesRequest, type SkeinRouteHandler, type SkeinRouteHandlerOptions, type SkeinRouteHandlers, createSkeinPagesHandler, createSkeinRouteHandlers, getSkeinRuntime, toWebResponse, webErrorResponse };
|
|
70
|
+
export { type SkeinInvokeRouteHandlerOptions, type SkeinInvokeRouteHandlers, type SkeinPagesHandler, type SkeinPagesHandlerOptions, type SkeinPagesRequest, type SkeinRouteHandler, type SkeinRouteHandlerOptions, type SkeinRouteHandlers, createSkeinInvokeRouteHandlers, createSkeinPagesHandler, createSkeinRouteHandlers, getSkeinInvokeDeps, getSkeinRuntime, toWebResponse, webErrorResponse };
|
package/dist/index.js
CHANGED
|
@@ -4,27 +4,31 @@ import {
|
|
|
4
4
|
matchSkeinRoute
|
|
5
5
|
} from "@skein-js/agent-protocol";
|
|
6
6
|
import { SkeinHttpError } from "@skein-js/core";
|
|
7
|
+
import { stripBasePath } from "@skein-js/server-kit";
|
|
7
8
|
|
|
8
9
|
// src/runtime-singleton.ts
|
|
9
10
|
import {
|
|
10
|
-
resolveProtocolRuntime
|
|
11
|
+
resolveProtocolRuntime,
|
|
12
|
+
resolveRuntimeDeps
|
|
11
13
|
} from "@skein-js/server-kit";
|
|
12
14
|
var CONFIG_CACHE_KEY = /* @__PURE__ */ Symbol.for("skein.nextjs.configRuntimeCache");
|
|
13
15
|
var DEPS_CACHE_KEY = /* @__PURE__ */ Symbol.for("skein.nextjs.depsRuntimeCache");
|
|
14
|
-
|
|
16
|
+
var CONFIG_INVOKE_CACHE_KEY = /* @__PURE__ */ Symbol.for("skein.nextjs.configInvokeCache");
|
|
17
|
+
var DEPS_INVOKE_CACHE_KEY = /* @__PURE__ */ Symbol.for("skein.nextjs.depsInvokeCache");
|
|
18
|
+
function globalMap(key) {
|
|
15
19
|
const store = globalThis;
|
|
16
|
-
store[
|
|
17
|
-
return store[
|
|
20
|
+
store[key] ??= /* @__PURE__ */ new Map();
|
|
21
|
+
return store[key];
|
|
18
22
|
}
|
|
19
|
-
function
|
|
23
|
+
function globalWeakMap(key) {
|
|
20
24
|
const store = globalThis;
|
|
21
|
-
store[
|
|
22
|
-
return store[
|
|
25
|
+
store[key] ??= /* @__PURE__ */ new WeakMap();
|
|
26
|
+
return store[key];
|
|
23
27
|
}
|
|
24
|
-
function resolveOnce(cache, key,
|
|
28
|
+
function resolveOnce(cache, key, resolve) {
|
|
25
29
|
let resolved = cache.get(key);
|
|
26
30
|
if (!resolved) {
|
|
27
|
-
resolved =
|
|
31
|
+
resolved = resolve().catch((error) => {
|
|
28
32
|
cache.delete(key);
|
|
29
33
|
throw error;
|
|
30
34
|
});
|
|
@@ -32,11 +36,27 @@ function resolveOnce(cache, key, options) {
|
|
|
32
36
|
}
|
|
33
37
|
return resolved;
|
|
34
38
|
}
|
|
35
|
-
function
|
|
39
|
+
function memoizeByOptions(options, configKey, depsKey, resolve) {
|
|
36
40
|
if (typeof options.config === "string") {
|
|
37
|
-
return resolveOnce(
|
|
41
|
+
return resolveOnce(globalMap(configKey), options.config, resolve);
|
|
38
42
|
}
|
|
39
|
-
return resolveOnce(
|
|
43
|
+
return resolveOnce(globalWeakMap(depsKey), options.deps, resolve);
|
|
44
|
+
}
|
|
45
|
+
function getSkeinRuntime(options) {
|
|
46
|
+
return memoizeByOptions(
|
|
47
|
+
options,
|
|
48
|
+
CONFIG_CACHE_KEY,
|
|
49
|
+
DEPS_CACHE_KEY,
|
|
50
|
+
() => resolveProtocolRuntime(options)
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
function getSkeinInvokeDeps(options) {
|
|
54
|
+
return memoizeByOptions(
|
|
55
|
+
options,
|
|
56
|
+
CONFIG_INVOKE_CACHE_KEY,
|
|
57
|
+
DEPS_INVOKE_CACHE_KEY,
|
|
58
|
+
() => resolveRuntimeDeps(options)
|
|
59
|
+
);
|
|
40
60
|
}
|
|
41
61
|
|
|
42
62
|
// src/send-web-response.ts
|
|
@@ -114,12 +134,6 @@ function preflightResponse(request, cors) {
|
|
|
114
134
|
}
|
|
115
135
|
|
|
116
136
|
// src/create-route-handlers.ts
|
|
117
|
-
function stripBasePath(pathname, basePath) {
|
|
118
|
-
if (basePath === "" || basePath === "/") return pathname;
|
|
119
|
-
if (pathname === basePath) return "/";
|
|
120
|
-
if (pathname.startsWith(`${basePath}/`)) return pathname.slice(basePath.length);
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
137
|
function toQuery(url) {
|
|
124
138
|
const query = {};
|
|
125
139
|
for (const key of new Set(url.searchParams.keys())) {
|
|
@@ -140,8 +154,10 @@ async function toProtocolRequest(request, url, skeinPathname, params) {
|
|
|
140
154
|
}
|
|
141
155
|
return {
|
|
142
156
|
method: request.method,
|
|
143
|
-
// Report the skein-relative absolute URL (mount prefix stripped)
|
|
144
|
-
//
|
|
157
|
+
// Report the skein-relative absolute URL (mount prefix stripped). NOTE: this does NOT match the
|
|
158
|
+
// Express/NestJS adapters, which report the full mount-inclusive URL (`req.originalUrl`). An auth
|
|
159
|
+
// handler that matches on `req.url`'s pathname therefore sees `/threads` here but `/api/threads`
|
|
160
|
+
// there. Worth unifying — but changing either side silently re-points existing auth rules.
|
|
145
161
|
url: `${url.origin}${skeinPathname}${url.search}`,
|
|
146
162
|
params,
|
|
147
163
|
query: toQuery(url),
|
|
@@ -204,14 +220,9 @@ import {
|
|
|
204
220
|
applyNodeCors,
|
|
205
221
|
sendNodeError,
|
|
206
222
|
sendNodePreflight,
|
|
207
|
-
sendNodeResponse
|
|
223
|
+
sendNodeResponse,
|
|
224
|
+
stripBasePath as stripBasePath2
|
|
208
225
|
} from "@skein-js/server-kit";
|
|
209
|
-
function stripBasePath2(pathname, basePath) {
|
|
210
|
-
if (basePath === "" || basePath === "/") return pathname;
|
|
211
|
-
if (pathname === basePath) return "/";
|
|
212
|
-
if (pathname.startsWith(`${basePath}/`)) return pathname.slice(basePath.length);
|
|
213
|
-
return null;
|
|
214
|
-
}
|
|
215
226
|
function toQuery2(url) {
|
|
216
227
|
const query = {};
|
|
217
228
|
for (const key of new Set(url.searchParams.keys())) {
|
|
@@ -284,11 +295,91 @@ function createSkeinPagesHandler(options) {
|
|
|
284
295
|
};
|
|
285
296
|
}
|
|
286
297
|
|
|
298
|
+
// src/create-invoke-route-handlers.ts
|
|
299
|
+
import {
|
|
300
|
+
createGraphInvokeHandler
|
|
301
|
+
} from "@skein-js/agent-protocol";
|
|
302
|
+
import { SkeinHttpError as SkeinHttpError2 } from "@skein-js/core";
|
|
303
|
+
function toQuery3(url) {
|
|
304
|
+
const query = {};
|
|
305
|
+
for (const key of new Set(url.searchParams.keys())) {
|
|
306
|
+
const all = url.searchParams.getAll(key);
|
|
307
|
+
query[key] = all.length > 1 ? all : all[0];
|
|
308
|
+
}
|
|
309
|
+
return query;
|
|
310
|
+
}
|
|
311
|
+
function graphIdFromPath(pathname, basePath) {
|
|
312
|
+
const prefix = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
|
|
313
|
+
if (!pathname.startsWith(`${prefix}/`)) return null;
|
|
314
|
+
const rest = pathname.slice(prefix.length + 1);
|
|
315
|
+
if (rest === "" || rest.includes("/")) return null;
|
|
316
|
+
try {
|
|
317
|
+
return decodeURIComponent(rest);
|
|
318
|
+
} catch {
|
|
319
|
+
return rest;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function createSkeinInvokeRouteHandlers(options) {
|
|
323
|
+
const basePath = options.basePath ?? "/api/invoke";
|
|
324
|
+
const logger = options.logger;
|
|
325
|
+
let invoke;
|
|
326
|
+
const post = async (request) => {
|
|
327
|
+
const url = new URL(request.url);
|
|
328
|
+
const graphId = graphIdFromPath(url.pathname, basePath);
|
|
329
|
+
if (graphId === null) return new Response(null, { status: 404 });
|
|
330
|
+
let resolved;
|
|
331
|
+
try {
|
|
332
|
+
resolved = await getSkeinInvokeDeps(options);
|
|
333
|
+
} catch (error) {
|
|
334
|
+
return webErrorResponse(error, {}, logger);
|
|
335
|
+
}
|
|
336
|
+
const cors = options.cors ?? resolved.cors;
|
|
337
|
+
const extraHeaders = cors ? corsHeaders(request, cors) : {};
|
|
338
|
+
try {
|
|
339
|
+
const text = await request.text();
|
|
340
|
+
let body;
|
|
341
|
+
try {
|
|
342
|
+
body = text ? JSON.parse(text) : {};
|
|
343
|
+
} catch {
|
|
344
|
+
throw SkeinHttpError2.badRequest("Request body is not valid JSON.");
|
|
345
|
+
}
|
|
346
|
+
invoke ??= createGraphInvokeHandler(resolved.deps, { streamMode: options.streamMode });
|
|
347
|
+
const response = await invoke({
|
|
348
|
+
method: request.method,
|
|
349
|
+
url: `${url.origin}${url.pathname}${url.search}`,
|
|
350
|
+
params: { graph_id: graphId },
|
|
351
|
+
query: toQuery3(url),
|
|
352
|
+
body,
|
|
353
|
+
headers: Object.fromEntries(request.headers),
|
|
354
|
+
// The Web `Request` aborts this when the client goes away, so the graph stops with it.
|
|
355
|
+
signal: request.signal
|
|
356
|
+
});
|
|
357
|
+
return toWebResponse(response, extraHeaders);
|
|
358
|
+
} catch (error) {
|
|
359
|
+
return webErrorResponse(error, extraHeaders, logger);
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
const optionsHandler = async (request) => {
|
|
363
|
+
let cors = options.cors;
|
|
364
|
+
if (cors === void 0) {
|
|
365
|
+
try {
|
|
366
|
+
cors = (await getSkeinInvokeDeps(options)).cors;
|
|
367
|
+
} catch {
|
|
368
|
+
cors = void 0;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return cors ? preflightResponse(request, cors) : new Response(null, { status: 404 });
|
|
372
|
+
};
|
|
373
|
+
return { POST: post, OPTIONS: optionsHandler };
|
|
374
|
+
}
|
|
375
|
+
|
|
287
376
|
// src/index.ts
|
|
288
377
|
import { sendNodeResponse as sendNodeResponse2, sendNodeError as sendNodeError2 } from "@skein-js/server-kit";
|
|
289
378
|
export {
|
|
379
|
+
createSkeinInvokeRouteHandlers,
|
|
290
380
|
createSkeinPagesHandler,
|
|
291
381
|
createSkeinRouteHandlers,
|
|
382
|
+
getSkeinInvokeDeps,
|
|
292
383
|
getSkeinRuntime,
|
|
293
384
|
sendNodeError2 as sendNodeError,
|
|
294
385
|
sendNodeResponse2 as sendNodeResponse,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skein-js/nextjs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Next.js adapter for skein-js — serve the Agent Protocol from App Router or Pages Router API routes.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Maina Wycliffe <wmmaina7@gmail.com>",
|
|
@@ -40,9 +40,9 @@
|
|
|
40
40
|
"node": ">=20"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@skein-js/agent-protocol": "0.
|
|
44
|
-
"@skein-js/
|
|
45
|
-
"@skein-js/
|
|
43
|
+
"@skein-js/agent-protocol": "0.9.1",
|
|
44
|
+
"@skein-js/core": "0.9.1",
|
|
45
|
+
"@skein-js/server-kit": "0.9.1"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"@langchain/langgraph": "^1.4.0",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@langchain/core": "^1.2.0",
|
|
53
53
|
"@langchain/langgraph": "^1.4.0",
|
|
54
|
-
"@skein-js/storage-memory": "0.
|
|
54
|
+
"@skein-js/storage-memory": "0.9.1"
|
|
55
55
|
},
|
|
56
56
|
"publishConfig": {
|
|
57
57
|
"access": "public"
|