@skein-js/nextjs 0.7.0 → 0.9.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
@@ -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';
1
+ import { SkeinRuntimeOptions, ResolvedRuntimeDeps, ResolvedProtocolRuntime } from '@skein-js/server-kit';
2
2
  export { 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 RuntimePromise = Promise<ResolvedProtocolRuntime>;
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): RuntimePromise;
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
- function configCache() {
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[CONFIG_CACHE_KEY] ??= /* @__PURE__ */ new Map();
17
- return store[CONFIG_CACHE_KEY];
20
+ store[key] ??= /* @__PURE__ */ new Map();
21
+ return store[key];
18
22
  }
19
- function depsCache() {
23
+ function globalWeakMap(key) {
20
24
  const store = globalThis;
21
- store[DEPS_CACHE_KEY] ??= /* @__PURE__ */ new WeakMap();
22
- return store[DEPS_CACHE_KEY];
25
+ store[key] ??= /* @__PURE__ */ new WeakMap();
26
+ return store[key];
23
27
  }
24
- function resolveOnce(cache, key, options) {
28
+ function resolveOnce(cache, key, resolve) {
25
29
  let resolved = cache.get(key);
26
30
  if (!resolved) {
27
- resolved = resolveProtocolRuntime(options).catch((error) => {
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 getSkeinRuntime(options) {
39
+ function memoizeByOptions(options, configKey, depsKey, resolve) {
36
40
  if (typeof options.config === "string") {
37
- return resolveOnce(configCache(), options.config, options);
41
+ return resolveOnce(globalMap(configKey), options.config, resolve);
38
42
  }
39
- return resolveOnce(depsCache(), options.deps, options);
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) so auth handlers see the same
144
- // path shape the other adapters produce.
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.7.0",
3
+ "version": "0.9.0",
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.7.0",
44
- "@skein-js/server-kit": "0.7.0",
45
- "@skein-js/core": "0.7.0"
43
+ "@skein-js/agent-protocol": "0.9.0",
44
+ "@skein-js/core": "0.9.0",
45
+ "@skein-js/server-kit": "0.9.0"
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.7.0"
54
+ "@skein-js/storage-memory": "0.9.0"
55
55
  },
56
56
  "publishConfig": {
57
57
  "access": "public"