@skein-js/server-kit 0.21.0 → 0.23.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
@@ -10,7 +10,7 @@ Part of **[skein-js](https://github.com/skein-js/skein-js)** — the open-source
10
10
 
11
11
  This package is the common ground the framework adapters ([`@skein-js/express`](../server-express),
12
12
  [`@skein-js/fastify`](../server-fastify), [`@skein-js/nestjs`](../server-nestjs),
13
- [`@skein-js/nextjs`](../server-nextjs)) stand on — so no adapter has to depend on another (or on
13
+ [`@skein-js/nextjs`](../server-nextjs), and [`@skein-js/fetch`](../server-fetch)) stand on — so no adapter has to depend on another (or on
14
14
  Express) just to reuse it. It holds the framework-agnostic pieces (runtime assembly, dev-state import,
15
15
  CORS, and the Node-`http` transport the Node-based adapters share); each adapter still writes only the
16
16
  thin request/response shim its framework needs over the [`@skein-js/agent-protocol`](../agent-protocol)
@@ -31,6 +31,11 @@ handler table.
31
31
  `describeSnapshot`, from the **`@skein-js/server-kit/dev` subpath**: read an existing
32
32
  `.langgraph_api/` directory and reconstruct skein's own `DevStateSnapshot`, so adopting skein carries
33
33
  all local state over losslessly.
34
+ - **Native HTTP and graph UI** — `loadConfigHttpHandler` loads exported Fetch/Hono applications
35
+ from `http.app` and graph UI from `ui`/`ui_config`. A 404 falls through with the original request
36
+ body preserved. `withNativePath`, `readNativeNodeRequest`, and `sendNativeResponse` share mounting
37
+ and streaming behavior across adapters; production assets are compiled by `skein build`. See
38
+ [CLI compatibility](https://skein-js.github.io/skein-js/langgraph-cli-compat#graph-ui-and-custom-http-applications).
34
39
  - **CORS** — `corsFromHttpConfig` / `toCorsOptions` map a `langgraph.json` `http.cors` block to
35
40
  `cors`-style `CorsOptions`; `allowedOrigin` / `corsResponseHeaders` / `applyNodeCors` /
36
41
  `sendNodePreflight` derive CORS headers for the adapters without a CORS middleware of their own
@@ -0,0 +1,213 @@
1
+ // src/channels-runtime.ts
2
+ import { createAuthScopedStore, createProtocolServiceFromContext } from "@skein-js/agent-protocol";
3
+ import { SkeinConfigError } from "@skein-js/config";
4
+ import {
5
+ SkeinHttpError
6
+ } from "@skein-js/core";
7
+ async function resolveChannels(input) {
8
+ const names = Object.keys(input.channels);
9
+ if (names.length === 0) return void 0;
10
+ const channels = await importChannelsPackage(names);
11
+ const registry = channels.buildChannelRegistry({
12
+ channels: Object.fromEntries(
13
+ Object.entries(input.channels).map(([name, loaded]) => [
14
+ name,
15
+ {
16
+ module: loaded.module,
17
+ config: {
18
+ assistant: loaded.assistant,
19
+ ...loaded.allowedAssistants ? { allowedAssistants: loaded.allowedAssistants } : {},
20
+ ...loaded.publicUrl ? { publicUrl: loaded.publicUrl } : {}
21
+ }
22
+ }
23
+ ])
24
+ ),
25
+ graphIds: input.graphIds
26
+ });
27
+ const channelInventory = registry.names.map((routeName) => {
28
+ const registered = registry.get(routeName);
29
+ if (!registered) throw new Error(`Channel registry lost configured route "${routeName}".`);
30
+ return {
31
+ route_name: routeName,
32
+ assistant: registered.config.assistant,
33
+ allowed_assistants: [...registered.config.allowedAssistants ?? []],
34
+ channel_name: registered.channel.name,
35
+ delivery_supported: typeof registered.channel.deliver === "function"
36
+ };
37
+ });
38
+ return {
39
+ routes: [
40
+ { method: "get", path: "/channels", handler: "listChannels" },
41
+ ...channels.channelRouteBindings(registry.names)
42
+ ],
43
+ wrapDispatcher: (inner) => channels.wrapChannelDispatcher({
44
+ registry,
45
+ inner,
46
+ logger: input.deps.logger ?? console
47
+ }),
48
+ handlers: (context) => ({
49
+ listChannels: async () => ({
50
+ kind: "json",
51
+ status: 200,
52
+ body: { channels: channelInventory }
53
+ }),
54
+ handleInboundEvent: async (req) => {
55
+ const name = req.url.split("?")[0].split("/channels/")[1] ?? "";
56
+ const registered = registry.get(name);
57
+ if (!registered) throw SkeinHttpError.notFound(`No channel named "${name}".`);
58
+ return channels.handleInboundEvent(
59
+ registered,
60
+ {
61
+ method: req.method,
62
+ url: req.url,
63
+ headers: req.headers,
64
+ ...typeof req.body === "string" ? { text: req.body } : {}
65
+ },
66
+ buildPipelineDeps(context, [
67
+ registered.config.assistant,
68
+ ...registered.config.allowedAssistants ?? []
69
+ ])
70
+ );
71
+ }
72
+ })
73
+ };
74
+ }
75
+ function buildPipelineDeps(context, allowedAssistants) {
76
+ const deps = context.deps;
77
+ const service = () => createProtocolServiceFromContext(context);
78
+ let requestFilters;
79
+ let verifiedContext;
80
+ const scopedService = (authContext, admission) => {
81
+ if (!authContext && !requestFilters && !admission) return service();
82
+ const base = context;
83
+ return createProtocolServiceFromContext({
84
+ ...base,
85
+ ...admission && deps.store.admission ? { admission } : {},
86
+ ...authContext?.user ? { authUser: authContext.user } : {},
87
+ ...authContext?.scopes ? { authScopes: authContext.scopes } : {},
88
+ deps: requestFilters && deps.auth ? {
89
+ ...base.deps,
90
+ store: createAuthScopedStore(base.deps.store, deps.auth, requestFilters, "threads")
91
+ } : base.deps
92
+ });
93
+ };
94
+ return {
95
+ ...deps.store.idempotency ? {
96
+ idempotency: {
97
+ ...deps.store.idempotency,
98
+ claim: async (input) => {
99
+ const authorizeRecorded = async (record) => {
100
+ const run = record.run_id ? await deps.store.runs.get(record.run_id) : null;
101
+ if (run) {
102
+ const assistant = await deps.store.assistants.get(run.assistant_id);
103
+ if (!allowedAssistants.includes(run.assistant_id) && !(assistant && allowedAssistants.includes(assistant.graph_id)))
104
+ throw SkeinHttpError.forbidden(
105
+ "Recorded assistant is outside the channel allowlist.",
106
+ { code: "assistant_not_allowed" }
107
+ );
108
+ }
109
+ if (!deps.auth) return;
110
+ const threadId = run?.thread_id ?? record.thread_id;
111
+ const decision = await deps.auth.authorize({
112
+ resource: "threads",
113
+ action: "create_run",
114
+ context: verifiedContext,
115
+ value: {
116
+ ...threadId ? { thread_id: threadId } : {},
117
+ ...run ? { run_id: run.run_id, assistant_id: run.assistant_id } : {}
118
+ }
119
+ });
120
+ const store = decision.filters ? createAuthScopedStore(deps.store, deps.auth, decision.filters, "threads") : deps.store;
121
+ if (threadId && !await store.threads.get(threadId))
122
+ throw SkeinHttpError.notFound("Thread not found.");
123
+ if (record.run_id && !await store.runs.get(record.run_id))
124
+ throw SkeinHttpError.notFound("Run not found.");
125
+ };
126
+ const accepted = deps.store.admission ? await deps.store.idempotency.get(`admission ${input.scope}`, input.key) : null;
127
+ if (accepted && accepted.expires_at > input.now && accepted.fingerprint === input.fingerprint && accepted.run_id) {
128
+ await authorizeRecorded(accepted);
129
+ return {
130
+ claimed: false,
131
+ record: {
132
+ ...accepted,
133
+ scope: input.scope,
134
+ response: { status: 202, body: { run_id: accepted.run_id } }
135
+ }
136
+ };
137
+ }
138
+ const result = await deps.store.idempotency.claim(input);
139
+ if (!result.claimed && result.record.status === "done")
140
+ await authorizeRecorded(result.record);
141
+ return result;
142
+ }
143
+ }
144
+ } : {},
145
+ clock: () => deps.clock ? deps.clock() : /* @__PURE__ */ new Date(),
146
+ logger: deps.logger ?? console,
147
+ // Straight off the run event bus — the same stream `runs.joinStream` serves, without the HTTP
148
+ // connection per conversation that reaching it from outside would cost.
149
+ runFrames: { subscribe: (runId) => deps.bus.subscribe(runId) },
150
+ authorize: async (principal, authz, value) => {
151
+ if (!deps.auth) return void 0;
152
+ const context2 = {
153
+ user: {
154
+ ...principal.metadata ?? {},
155
+ identity: principal.identity,
156
+ display_name: principal.identity,
157
+ is_authenticated: true,
158
+ permissions: principal.permissions ?? []
159
+ },
160
+ scopes: principal.permissions ?? []
161
+ };
162
+ verifiedContext = context2;
163
+ const decision = await deps.auth.authorize({
164
+ resource: authz.resource,
165
+ action: authz.action,
166
+ value: value ?? {},
167
+ context: context2
168
+ });
169
+ requestFilters = decision.filters;
170
+ return context2;
171
+ },
172
+ ensureThread: async (threadId, metadata, authContext) => {
173
+ await scopedService(authContext).threads.create({
174
+ thread_id: threadId,
175
+ metadata,
176
+ ifExists: "do_nothing"
177
+ });
178
+ },
179
+ threadStatus: async (threadId) => {
180
+ const thread = await scopedService(void 0).threads.get(threadId).catch(() => null);
181
+ return thread?.status ?? null;
182
+ },
183
+ createRun: async (input) => {
184
+ const run = await scopedService(input.authContext, input.admission).runs.createBackground(
185
+ input.threadId,
186
+ {
187
+ assistant_id: input.assistantId,
188
+ ...input.input !== void 0 ? { input: input.input } : {},
189
+ ...input.command ? { command: input.command } : {},
190
+ ...input.ifThreadStatus ? { if_thread_status: [...input.ifThreadStatus] } : {},
191
+ ...input.multitaskStrategy ? { multitask_strategy: input.multitaskStrategy } : {},
192
+ ...input.metadata ? { metadata: input.metadata } : {},
193
+ ...input.webhook ? { webhook: input.webhook } : {},
194
+ stream_mode: [...input.streamMode]
195
+ }
196
+ );
197
+ return { runId: run.run_id };
198
+ }
199
+ };
200
+ }
201
+ async function importChannelsPackage(names) {
202
+ try {
203
+ return await import("@skein-js/channels");
204
+ } catch (cause) {
205
+ throw new SkeinConfigError(
206
+ `skein.channels configures ${names.join(", ")}, but @skein-js/channels is not installed. Add it as a dependency.`,
207
+ { cause }
208
+ );
209
+ }
210
+ }
211
+ export {
212
+ resolveChannels
213
+ };
@@ -12,6 +12,286 @@ import {
12
12
  } from "@skein-js/config";
13
13
  import { MemorySkeinStore as MemorySkeinStore2 } from "@skein-js/storage-memory";
14
14
 
15
+ // src/config-http-handler.ts
16
+ import { pathToFileURL } from "url";
17
+ import {
18
+ parseGraphSpec,
19
+ SkeinConfigError
20
+ } from "@skein-js/config";
21
+
22
+ // src/graph-ui.ts
23
+ import { readFile } from "fs/promises";
24
+ import path from "path";
25
+ import { SkeinHttpError as SkeinHttpError2 } from "@skein-js/core";
26
+
27
+ // src/native-http.ts
28
+ import { serializeWireJson, SkeinHttpError } from "@skein-js/core";
29
+ var sourceUrls = /* @__PURE__ */ new WeakMap();
30
+ function withNativePath(request, pathname) {
31
+ const url = new URL(request.url);
32
+ url.pathname = pathname;
33
+ const native = new Request(url, request.clone());
34
+ sourceUrls.set(native, sourceUrls.get(request) ?? request.url);
35
+ return native;
36
+ }
37
+ function originalNativeUrl(request) {
38
+ return sourceUrls.get(request) ?? request.url;
39
+ }
40
+ function toNativeRequest(request, rawBody) {
41
+ const headers = new Headers();
42
+ for (const [name, value] of Object.entries(request.headers))
43
+ if (value !== void 0) headers.set(name, value);
44
+ const hasBody = !["GET", "HEAD"].includes(request.method.toUpperCase());
45
+ const body = !hasBody ? void 0 : rawBody ?? (request.body === void 0 ? void 0 : typeof request.body === "string" || request.body instanceof Uint8Array ? request.body : serializeWireJson(request.body));
46
+ if (!rawBody) headers.delete("content-length");
47
+ return new Request(request.url, {
48
+ method: request.method,
49
+ headers,
50
+ body: body instanceof Uint8Array ? new Uint8Array(body) : body,
51
+ signal: request.signal
52
+ });
53
+ }
54
+ async function readNativeNodeRequest(request, url, signal, handler) {
55
+ const matcher = handler?.[/* @__PURE__ */ Symbol.for("@skein-js/native-request-matcher")];
56
+ if (matcher?.(request.method ?? "GET", url) === false) return void 0;
57
+ const headers = {};
58
+ for (const [name, value] of Object.entries(request.headers))
59
+ headers[name] = Array.isArray(value) ? value.join(", ") : value;
60
+ let rawBody = request.rawBody;
61
+ if (!rawBody && request.body === void 0 && !["GET", "HEAD"].includes(request.method ?? "GET") && !request.readableEnded) {
62
+ const chunks = [];
63
+ let size = 0;
64
+ for await (const chunk of request) {
65
+ const bytes = chunk instanceof Uint8Array ? chunk : Buffer.from(String(chunk));
66
+ size += bytes.byteLength;
67
+ if (size > 100 * 1024)
68
+ throw SkeinHttpError.payloadTooLarge("Request body exceeds 102400 bytes.");
69
+ chunks.push(bytes);
70
+ }
71
+ rawBody = Buffer.concat(chunks);
72
+ request.rawBody = rawBody;
73
+ const text = new TextDecoder().decode(rawBody);
74
+ try {
75
+ request.body = headers["content-type"]?.includes("application/json") ? text ? JSON.parse(text) : {} : text;
76
+ } catch {
77
+ throw SkeinHttpError.badRequest("Request body is not valid JSON.");
78
+ }
79
+ }
80
+ const native = toNativeRequest(
81
+ { method: request.method ?? "GET", url, headers, body: request.body, signal },
82
+ rawBody
83
+ );
84
+ sourceUrls.set(native, new URL(request.originalUrl ?? request.url ?? url, url).href);
85
+ return native;
86
+ }
87
+ async function sendNativeResponse(response, target) {
88
+ target.statusCode = response.status;
89
+ for (const [name, value] of response.headers)
90
+ if (name !== "set-cookie") target.setHeader(name, value);
91
+ const cookies = response.headers.getSetCookie();
92
+ if (cookies.length) target.setHeader("set-cookie", cookies);
93
+ if (!response.body) {
94
+ target.end();
95
+ return;
96
+ }
97
+ const reader = response.body.getReader();
98
+ const close = () => {
99
+ void reader.cancel().catch(() => void 0);
100
+ };
101
+ target.once("close", close);
102
+ try {
103
+ for (; ; ) {
104
+ const chunk = await reader.read();
105
+ if (chunk.done || target.destroyed) break;
106
+ if (!target.write(chunk.value))
107
+ await new Promise((resolve) => {
108
+ const done = () => {
109
+ target.removeListener("drain", done);
110
+ target.removeListener("close", done);
111
+ resolve();
112
+ };
113
+ target.once("drain", done);
114
+ target.once("close", done);
115
+ });
116
+ }
117
+ } finally {
118
+ target.removeListener("close", close);
119
+ reader.releaseLock();
120
+ if (!target.writableEnded) target.end();
121
+ }
122
+ }
123
+
124
+ // src/graph-ui.ts
125
+ var watches = /* @__PURE__ */ new Map();
126
+ function escapeAttribute(value) {
127
+ return value.replace(
128
+ /[&<>"']/g,
129
+ (character) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[character]
130
+ );
131
+ }
132
+ async function readCompiledUi(config, configDir) {
133
+ const root = path.join(configDir, "ui");
134
+ let manifest;
135
+ try {
136
+ manifest = JSON.parse(
137
+ await readFile(path.join(root, "schemas.json"), "utf8")
138
+ );
139
+ } catch (error) {
140
+ if (error.code === "ENOENT") return void 0;
141
+ throw error;
142
+ }
143
+ const files = /* @__PURE__ */ new Map();
144
+ for (const graphId of Object.keys(config.ui ?? {})) {
145
+ const assets = manifest[graphId]?.assets;
146
+ if (!Array.isArray(assets) || !assets.length)
147
+ throw new Error(`Built UI manifest has no assets for "${graphId}".`);
148
+ const loaded = [];
149
+ for (const basename of assets) {
150
+ if (typeof basename !== "string" || basename !== path.basename(basename) || basename === "." || basename === "..")
151
+ throw new Error("Built UI manifest contains an invalid asset name.");
152
+ const filename = path.resolve(root, "public", graphId, basename);
153
+ if (!filename.startsWith(`${path.resolve(root, "public")}${path.sep}`))
154
+ throw new Error("Built UI asset escapes its artifact directory.");
155
+ loaded.push({ basename, contents: new Uint8Array(await readFile(filename)) });
156
+ }
157
+ files.set(graphId, loaded);
158
+ }
159
+ return files;
160
+ }
161
+ async function loadGraphUiHandler(config, configDir) {
162
+ if (!config.ui || !Object.keys(config.ui).length) return void 0;
163
+ let files = await readCompiledUi(config, configDir);
164
+ if (!files) {
165
+ const key = JSON.stringify([configDir, config.ui, config.ui_config]);
166
+ let pending = watches.get(key);
167
+ if (!pending) {
168
+ pending = (async () => {
169
+ const { build, watch } = await import(
170
+ /* webpackIgnore: true */
171
+ "@langchain/langgraph-ui"
172
+ );
173
+ await build({
174
+ cwd: configDir,
175
+ defs: config.ui,
176
+ config: config.ui_config,
177
+ output: ".skein/ui"
178
+ });
179
+ const results = await readCompiledUi(config, path.join(configDir, ".skein"));
180
+ await watch({
181
+ cwd: configDir,
182
+ defs: config.ui,
183
+ config: config.ui_config,
184
+ onOutput: (graphId, output) => {
185
+ results.set(graphId, output);
186
+ }
187
+ });
188
+ return results;
189
+ })();
190
+ watches.set(key, pending);
191
+ void pending.catch(() => watches.delete(key));
192
+ }
193
+ files = await pending;
194
+ }
195
+ const registry = files;
196
+ return async (request) => {
197
+ const url = new URL(request.url);
198
+ const match = /^\/ui\/([^/]+)(?:\/([^/]+))?$/.exec(url.pathname);
199
+ if (!match) return void 0;
200
+ let graphId;
201
+ let basename;
202
+ try {
203
+ graphId = decodeURIComponent(match[1]);
204
+ basename = match[2] ? decodeURIComponent(match[2]) : void 0;
205
+ } catch {
206
+ throw SkeinHttpError2.badRequest("Invalid UI path encoding.");
207
+ }
208
+ const assets = registry.get(graphId);
209
+ if (!assets?.length) return new Response("UI not found", { status: 404 });
210
+ if (request.method === "GET" && basename) {
211
+ const asset = assets.find((file) => file.basename === basename);
212
+ if (!asset) return new Response("Asset not found", { status: 404 });
213
+ const mime = {
214
+ ".js": "text/javascript",
215
+ ".css": "text/css",
216
+ ".json": "application/json",
217
+ ".svg": "image/svg+xml",
218
+ ".png": "image/png",
219
+ ".woff2": "font/woff2"
220
+ };
221
+ return new Response(new Uint8Array(asset.contents), {
222
+ headers: {
223
+ "content-type": mime[path.extname(basename)] ?? "application/octet-stream",
224
+ "cache-control": "no-cache"
225
+ }
226
+ });
227
+ }
228
+ if (request.method !== "POST" || basename) return void 0;
229
+ let body;
230
+ try {
231
+ body = await request.json();
232
+ } catch {
233
+ throw SkeinHttpError2.badRequest("UI bootstrap requires JSON.");
234
+ }
235
+ if (typeof body !== "object" || body === null || !("name" in body) || typeof body.name !== "string")
236
+ throw SkeinHttpError2.badRequest("UI bootstrap requires a component name.");
237
+ const assetUrl = (basename2) => escapeAttribute(
238
+ new URL(
239
+ `${encodeURIComponent(graphId)}/${encodeURIComponent(basename2)}`,
240
+ originalNativeUrl(request)
241
+ ).href
242
+ );
243
+ const html = assets.filter((file) => file.basename.endsWith(".css")).map((file) => `<link rel="stylesheet" href="${assetUrl(file.basename)}" />`);
244
+ const script = assets.find((file) => file.basename.endsWith(".js"));
245
+ if (script) {
246
+ const stableName = graphId.replace(/[^a-zA-Z0-9]/g, "_");
247
+ const render = `__LGUI_${stableName}.render(${JSON.stringify(body.name)}, "{{shadowRootId}}")`;
248
+ html.push(
249
+ `<script src="${assetUrl(script.basename)}" onload="${escapeAttribute(render)}"></script>`
250
+ );
251
+ }
252
+ return new Response(html.join("\n"), {
253
+ headers: { "content-type": "text/html; charset=utf-8" }
254
+ });
255
+ };
256
+ }
257
+
258
+ // src/config-http-handler.ts
259
+ async function loadConfigHttpHandler(config, options) {
260
+ const app = config.http?.["app"];
261
+ const ui = await loadGraphUiHandler(config, options.configDir);
262
+ if (app === void 0) return ui;
263
+ if (typeof app !== "string") throw new SkeinConfigError("http.app must be a path:export string.");
264
+ const spec = parseGraphSpec(app, options.configDir);
265
+ const importer = options.importModule ?? ((file) => import(pathToFileURL(file).href));
266
+ const module = await importer(spec.sourceFile);
267
+ const exported = module[spec.exportSymbol];
268
+ let fetch;
269
+ if (typeof exported === "function") fetch = exported;
270
+ else if (typeof exported === "object" && exported !== null && "fetch" in exported && typeof exported.fetch === "function")
271
+ fetch = exported.fetch.bind(exported);
272
+ else
273
+ throw new SkeinConfigError(
274
+ `http.app "${app}" must export a Fetch function or an object with fetch().`
275
+ );
276
+ return async (request) => {
277
+ const applicationRequest = request.clone();
278
+ let response;
279
+ try {
280
+ response = await fetch(applicationRequest);
281
+ if (!(response instanceof Response)) throw new TypeError("http.app must return a Response.");
282
+ if (response.status === 404) {
283
+ void response.body?.cancel().catch(() => void 0);
284
+ return ui?.(request);
285
+ }
286
+ void request.body?.cancel().catch(() => void 0);
287
+ return response;
288
+ } finally {
289
+ if (!response || response.status === 404 || response.body !== applicationRequest.body)
290
+ void applicationRequest.body?.cancel().catch(() => void 0);
291
+ }
292
+ };
293
+ }
294
+
15
295
  // src/cors-config.ts
16
296
  import {
17
297
  filterSkeinRoutes,
@@ -53,6 +333,8 @@ function corsFromHttpConfig(http) {
53
333
  return toCorsOptions(cors);
54
334
  }
55
335
  var DISABLE_FLAGS = {
336
+ disable_mcp: "mcp",
337
+ disable_a2a: "a2a",
56
338
  disable_assistants: "assistants",
57
339
  disable_threads: "threads",
58
340
  disable_runs: "runs",
@@ -102,20 +384,26 @@ function resolveIdempotency(raw) {
102
384
  import { MemorySaver } from "@langchain/langgraph";
103
385
  import {
104
386
  cloneLangGraphCheckpoint,
387
+ inspectMemoryThreadStorage,
105
388
  normalizeEmbeddableGraphs,
106
389
  SkeinBaseStore
107
390
  } from "@skein-js/langgraph";
108
- import { MemoryRunEventBus, MemoryRunQueue, MemorySkeinStore } from "@skein-js/storage-memory";
391
+ import {
392
+ MemoryRunEventBus,
393
+ MemoryThreadEventBus,
394
+ MemoryRunQueue,
395
+ MemorySkeinStore
396
+ } from "@skein-js/storage-memory";
109
397
 
110
398
  // src/max-page-size.ts
111
399
  import { DEFAULT_MAX_PAGE_SIZE } from "@skein-js/core";
112
400
 
113
401
  // src/env-numbers.ts
114
- import { SkeinConfigError } from "@skein-js/config/errors";
402
+ import { SkeinConfigError as SkeinConfigError2 } from "@skein-js/config/errors";
115
403
  function requirePositiveInteger(source, raw) {
116
404
  const parsed = Number(raw);
117
405
  if (!Number.isSafeInteger(parsed) || parsed <= 0) {
118
- throw new SkeinConfigError(`${source} must be a positive integer (got "${String(raw)}").`);
406
+ throw new SkeinConfigError2(`${source} must be a positive integer (got "${String(raw)}").`);
119
407
  }
120
408
  return parsed;
121
409
  }
@@ -196,6 +484,7 @@ import {
196
484
  } from "@skein-js/langgraph";
197
485
  function embedInMemoryGraphs(graphs, overrides = {}) {
198
486
  const webhooks = resolveWebhooks(void 0);
487
+ const checkpointer = overrides.checkpointer ?? new MemorySaver();
199
488
  return {
200
489
  // `maxPageSize` resolved for the same reason as the bus limits below: an embedded host's
201
490
  // `SKEIN_MAX_PAGE_SIZE` has to reach the driver that applies it.
@@ -205,7 +494,11 @@ function embedInMemoryGraphs(graphs, overrides = {}) {
205
494
  // Resolved rather than defaulted, so `SKEIN_MEMORY_BUS_*` reaches an embedded host too — this is
206
495
  // the driver set a long-lived single-process deployment actually runs on.
207
496
  bus: new MemoryRunEventBus(resolveMemoryBusLimits()),
208
- checkpointer: new MemorySaver(),
497
+ threadBus: new MemoryThreadEventBus(),
498
+ checkpointer,
499
+ ...checkpointer instanceof MemorySaver ? {
500
+ inspectThreadStorage: (threadId) => inspectMemoryThreadStorage(checkpointer, threadId)
501
+ } : {},
209
502
  // Bridges long-term memory into a LangGraph `BaseStore` so nodes reach it via `getStore()`. A
210
503
  // function on deps rather than an import inside the engine — see `ProtocolDeps.storeBridge`.
211
504
  storeBridge: (store) => new SkeinBaseStore(store),
@@ -229,6 +522,7 @@ var createInMemoryDeps = embedInMemoryGraphs;
229
522
  function resolveThreadTtl(raw) {
230
523
  if (!raw) return void 0;
231
524
  const ttl = {};
525
+ if (raw.strategy === "keep_latest") ttl.strategy = raw.strategy;
232
526
  if (typeof raw.default_ttl === "number") ttl.defaultTtl = raw.default_ttl;
233
527
  if (typeof raw.sweep_interval_minutes === "number") {
234
528
  ttl.sweepIntervalMinutes = raw.sweep_interval_minutes;
@@ -262,6 +556,7 @@ async function loadInMemoryRuntime(configPath, importModule, staticSchemas) {
262
556
  });
263
557
  const deps = embedInMemoryGraphs(toGraphResolver(graphs));
264
558
  deps.auth = await loadAuthEngine(config.auth, { configDir, importModule });
559
+ deps.httpHandler = await loadConfigHttpHandler(config, { configDir, importModule });
265
560
  return {
266
561
  deps,
267
562
  channels: await loadChannels(config.skein?.channels, {
@@ -296,6 +591,10 @@ async function loadReloadableInMemoryRuntime(configPath, importModule, staticSch
296
591
  store,
297
592
  checkpointer,
298
593
  auth: await loadAuthEngine(first.config.auth, { configDir: first.configDir, importModule }),
594
+ httpHandler: await loadConfigHttpHandler(first.config, {
595
+ configDir: first.configDir,
596
+ importModule
597
+ }),
299
598
  // Present only when configured — its presence is what starts the thread TTL sweeper.
300
599
  ...threadTtl ? { threadTtl } : {},
301
600
  // Unlike `threadTtl`, absence here means *defaults*, not *off*: the header is honoured either way.
@@ -319,7 +618,13 @@ async function loadReloadableInMemoryRuntime(configPath, importModule, staticSch
319
618
  config: first.config,
320
619
  configDir: first.configDir,
321
620
  reloadGraphs: async () => {
322
- current = (await loadConfig({ configPath, importModule, staticSchemas })).graphs;
621
+ const next = await loadConfig({ configPath, importModule, staticSchemas });
622
+ const httpHandler = await loadConfigHttpHandler(next.config, {
623
+ configDir: next.configDir,
624
+ importModule
625
+ });
626
+ current = next.graphs;
627
+ deps.httpHandler = httpHandler;
323
628
  },
324
629
  snapshotState: () => ({
325
630
  version: 1,
@@ -336,6 +641,11 @@ async function loadReloadableInMemoryRuntime(configPath, importModule, staticSch
336
641
  export {
337
642
  requirePositiveInteger,
338
643
  positiveIntegerFromEnv,
644
+ withNativePath,
645
+ toNativeRequest,
646
+ readNativeNodeRequest,
647
+ sendNativeResponse,
648
+ loadConfigHttpHandler,
339
649
  toCorsOptions,
340
650
  corsFromHttpConfig,
341
651
  disabledRoutesFromHttpConfig,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  loadInMemoryRuntime,
3
3
  loadReloadableInMemoryRuntime
4
- } from "./chunk-UJZBVPYI.js";
4
+ } from "./chunk-3IKRBGJU.js";
5
5
  import "./chunk-JUBBCC7R.js";
6
6
  export {
7
7
  loadInMemoryRuntime,
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ProtocolRuntime, RouteBinding, Logger, ProtocolDeps, RunWorkerOptions, CronSchedulerOptions, RunFailureReport, IdempotencyConfig, WebhookDeliveryConfig, GraphResolver, RouteMatcher, DisabledRouteGroups, ProtocolResponse } from '@skein-js/agent-protocol';
1
+ import { ProtocolRuntime, RouteBinding, Logger, ProtocolDeps, RunWorkerOptions, CronSchedulerOptions, RunFailureReport, IdempotencyConfig, WebhookDeliveryConfig, GraphResolver, RouteMatcher, DisabledRouteGroups, ProtocolResponse, ProtocolRequest } from '@skein-js/agent-protocol';
2
2
  export { CompiledGraphFactory, DEFAULT_RUN_CONCURRENCY, DEFAULT_SHUTDOWN_GRACE_MS, GraphResolver, Logger, ProtocolDeps, ResolvedGraph, RunWorkerOptions } from '@skein-js/agent-protocol';
3
3
  import { LoadedChannel, ModuleImporter, LanggraphJson, GraphSchemas } from '@skein-js/config';
4
4
  import { CorsOptions } from 'cors';
@@ -9,9 +9,11 @@ export { a as DevStateCounts } from './dev-Zz4VRv7T.js';
9
9
  import { StoreTtlConfig, ThreadTtlConfig } from '@skein-js/core';
10
10
  import { EmbeddableGraph } from '@skein-js/langgraph';
11
11
  export { EmbeddableGraph, graphMapToResolver, normalizeEmbeddableGraphs } from '@skein-js/langgraph';
12
- import { ServerResponse } from 'node:http';
12
+ import { ServerResponse, IncomingMessage } from 'node:http';
13
13
 
14
14
  interface SkeinRuntimeCommonOptions {
15
+ /** Override the mounted protocol route inventory, including discovery. */
16
+ routes?: readonly RouteBinding[];
15
17
  /**
16
18
  * Where skein reports itself: failed runs, webhook delivery failures, background-run summaries, and
17
19
  * the adapter's own transport faults. It reaches **the run engine**, not just the transport — this
@@ -469,15 +471,14 @@ extraDeps?: Partial<Omit<ProtocolDeps, "graphs">>): Promise<ReloadableInMemoryRu
469
471
  /** The raw `checkpointer.ttl` block as `langgraph.json` spells it. */
470
472
  interface RawThreadTtl {
471
473
  default_ttl?: number;
472
- strategy?: "delete";
474
+ strategy?: "delete" | "keep_latest";
473
475
  sweep_interval_minutes?: number;
474
476
  }
475
477
  /**
476
478
  * Map the raw block to {@link ThreadTtlConfig}, or `undefined` when nothing is set — so threads never
477
479
  * expire unless asked to, and no sweeper is started.
478
480
  *
479
- * `strategy` is read only to be validated by the config schema: `"delete"` is the only thing an
480
- * expired thread can become, so there is nothing for the drivers to branch on.
481
+ * Deletion stays the default; keep_latest is explicit so existing deployments retain their policy.
481
482
  */
482
483
  declare function resolveThreadTtl(raw: RawThreadTtl | undefined): ThreadTtlConfig | undefined;
483
484
  /** The raw `store.ttl` block as `langgraph.json` spells it. */
@@ -716,4 +717,25 @@ type RuntimeGlobal = typeof globalThis & {
716
717
  /** Detect Node, Bun, or Deno without importing a runtime-specific module. */
717
718
  declare function detectRuntimeCapabilities(scope?: RuntimeGlobal): RuntimeCapabilities;
718
719
 
719
- export { type ConsoleLogLevel, type ConsoleLoggerOptions, type CorsSetting, DEFAULT_HEAP_SAMPLE_MS, DEFAULT_HEAP_WARN_PERCENT, DevStateSnapshot, type FileReader, HEAP_REARM_MARGIN_PERCENT, HEAP_REARM_PERCENT, type HeapHeadroom, type HeapPressureContext, type HeapPressureMonitor, type HeapPressureMonitorOptions, type InMemoryRuntimeConfig, type IntervalHandle, type LanggraphCorsConfig, type RawIdempotency, type RawStoreTtl, type RawThreadTtl, type RawWebhooks, type ReloadableInMemoryRuntime, type ResolvedMemoryBusLimits, type ResolvedProtocolRuntime, type ResolvedRuntimeDeps, type RuntimeCapabilities, type RuntimeMemorySnapshot, type RuntimeSignalSubscription, type SkeinRuntimeCommonOptions, type SkeinRuntimeOptions, allowedOrigin, applyNodeCors, checkHeapHeadroom, consoleMountFromHttpConfig, corsFromHttpConfig, corsPreflightHeaders, corsResponseHeaders, createConsoleLogger, createInMemoryDeps, describeError, describeHeapHeadroom, describePoolPressure, detectRuntimeCapabilities, disabledRoutesFromHttpConfig, embedInMemoryGraphs, formatLogMeta, joinList, loadInMemoryRuntime, loadReloadableInMemoryRuntime, readContainerMemoryLimit, resolveCronTickMs, resolveHeapPressureOptions, resolveIdempotency, resolveMaxPageSize, resolveMemoryBusLimits, resolveProtocolRuntime, resolveRunConcurrency, resolveRunTimeoutMs, resolveRuntimeDeps, resolveShutdownGraceMs, resolveStoreTtl, resolveThreadTtl, resolveWebhooks, routeMatcherFor, routesFromHttpConfig, runFailureIdentity, sendNodeError, sendNodePreflight, sendNodeResponse, startHeapPressureMonitor, stripBasePath, toCorsOptions };
720
+ /** Load the same path:export notation as graphs; preserve Hono's receiver for its fetch method. */
721
+ declare function loadConfigHttpHandler(config: LanggraphJson, options: {
722
+ configDir: string;
723
+ importModule?: ModuleImporter;
724
+ }): Promise<((request: Request) => Promise<Response | undefined>) | undefined>;
725
+
726
+ /** Strip a mount path for app dispatch while retaining the public URL for asset references. */
727
+ declare function withNativePath(request: Request, pathname: string): Request;
728
+ /** Build a Fetch request from a framework's normalized body without changing protocol responses. */
729
+ declare function toNativeRequest(request: Pick<ProtocolRequest, "method" | "url" | "headers" | "body" | "signal">, rawBody?: Uint8Array): Request;
730
+ /** Native Node bridge also handles binary/form bodies when the host has not parsed them. */
731
+ type NativeNodeRequest = IncomingMessage & {
732
+ body?: unknown;
733
+ rawBody?: Uint8Array;
734
+ originalUrl?: string;
735
+ };
736
+ declare function readNativeNodeRequest(request: NativeNodeRequest, url: string, signal?: AbortSignal): Promise<Request>;
737
+ declare function readNativeNodeRequest(request: NativeNodeRequest, url: string, signal: AbortSignal | undefined, handler: (request: Request) => Promise<Response | undefined>): Promise<Request | undefined>;
738
+ /** Stream binary, HTML, JSON or SSE from a native app with backpressure and cancellation. */
739
+ declare function sendNativeResponse(response: Response, target: ServerResponse): Promise<void>;
740
+
741
+ export { type ConsoleLogLevel, type ConsoleLoggerOptions, type CorsSetting, DEFAULT_HEAP_SAMPLE_MS, DEFAULT_HEAP_WARN_PERCENT, DevStateSnapshot, type FileReader, HEAP_REARM_MARGIN_PERCENT, HEAP_REARM_PERCENT, type HeapHeadroom, type HeapPressureContext, type HeapPressureMonitor, type HeapPressureMonitorOptions, type InMemoryRuntimeConfig, type IntervalHandle, type LanggraphCorsConfig, type RawIdempotency, type RawStoreTtl, type RawThreadTtl, type RawWebhooks, type ReloadableInMemoryRuntime, type ResolvedMemoryBusLimits, type ResolvedProtocolRuntime, type ResolvedRuntimeDeps, type RuntimeCapabilities, type RuntimeMemorySnapshot, type RuntimeSignalSubscription, type SkeinRuntimeCommonOptions, type SkeinRuntimeOptions, allowedOrigin, applyNodeCors, checkHeapHeadroom, consoleMountFromHttpConfig, corsFromHttpConfig, corsPreflightHeaders, corsResponseHeaders, createConsoleLogger, createInMemoryDeps, describeError, describeHeapHeadroom, describePoolPressure, detectRuntimeCapabilities, disabledRoutesFromHttpConfig, embedInMemoryGraphs, formatLogMeta, joinList, loadConfigHttpHandler, loadInMemoryRuntime, loadReloadableInMemoryRuntime, readContainerMemoryLimit, readNativeNodeRequest, resolveCronTickMs, resolveHeapPressureOptions, resolveIdempotency, resolveMaxPageSize, resolveMemoryBusLimits, resolveProtocolRuntime, resolveRunConcurrency, resolveRunTimeoutMs, resolveRuntimeDeps, resolveShutdownGraceMs, resolveStoreTtl, resolveThreadTtl, resolveWebhooks, routeMatcherFor, routesFromHttpConfig, runFailureIdentity, sendNativeResponse, sendNodeError, sendNodePreflight, sendNodeResponse, startHeapPressureMonitor, stripBasePath, toCorsOptions, toNativeRequest, withNativePath };
package/dist/index.js CHANGED
@@ -5,10 +5,12 @@ import {
5
5
  disabledRoutesFromHttpConfig,
6
6
  embedInMemoryGraphs,
7
7
  graphMapToResolver,
8
+ loadConfigHttpHandler,
8
9
  loadInMemoryRuntime,
9
10
  loadReloadableInMemoryRuntime,
10
11
  normalizeEmbeddableGraphs,
11
12
  positiveIntegerFromEnv,
13
+ readNativeNodeRequest,
12
14
  requirePositiveInteger,
13
15
  resolveIdempotency,
14
16
  resolveMaxPageSize,
@@ -17,8 +19,11 @@ import {
17
19
  resolveThreadTtl,
18
20
  resolveWebhooks,
19
21
  routesFromHttpConfig,
20
- toCorsOptions
21
- } from "./chunk-UJZBVPYI.js";
22
+ sendNativeResponse,
23
+ toCorsOptions,
24
+ toNativeRequest,
25
+ withNativePath
26
+ } from "./chunk-3IKRBGJU.js";
22
27
  import "./chunk-JUBBCC7R.js";
23
28
 
24
29
  // src/resolve-runtime.ts
@@ -168,13 +173,13 @@ function resolveLogger(option, injected, frameworkLogger) {
168
173
  return option ?? frameworkLogger;
169
174
  }
170
175
  async function resolveRuntimeDeps(options, frameworkLogger) {
171
- const loaded = options.deps ? { deps: options.deps, cors: void 0, routes: skeinRoutes, channels: options.channels } : await (await import("./in-memory-runtime-IOQZUNLV.js")).loadInMemoryRuntime(options.config, options.importModule);
176
+ const loaded = options.deps ? { deps: options.deps, cors: void 0, routes: skeinRoutes, channels: options.channels } : await (await import("./in-memory-runtime-D4PEZVMU.js")).loadInMemoryRuntime(options.config, options.importModule);
172
177
  const logger = resolveLogger(options.logger, loaded.deps.logger, frameworkLogger);
173
178
  if (logger && !loaded.deps.logger) loaded.deps.logger = logger;
174
179
  return {
175
180
  deps: loaded.deps,
176
181
  cors: loaded.cors,
177
- routes: loaded.routes,
182
+ routes: options.routes ?? loaded.routes,
178
183
  ...loaded.channels ? { channels: loaded.channels } : {},
179
184
  logger
180
185
  };
@@ -190,22 +195,18 @@ async function resolveProtocolRuntime(options, frameworkLogger) {
190
195
  const heapPressureOptions = resolveHeapPressureOptions();
191
196
  const runTimeoutMs = resolveRunTimeoutMs(deps.runTimeoutMs);
192
197
  if (runTimeoutMs !== void 0) deps.runTimeoutMs = runTimeoutMs;
193
- const runtimeRef = {};
194
- const channelSurface = channels ? await (await import("./channels-runtime-JHTMXARF.js")).resolveChannels({
198
+ const channelSurface = channels ? await (await import("./channels-runtime-5TESQBKO.js")).resolveChannels({
195
199
  channels,
196
200
  graphIds: deps.graphs.ids,
197
- deps,
198
- service: () => {
199
- const current = runtimeRef.current;
200
- if (!current) throw new Error("channel pipeline used before the runtime was assembled");
201
- return current.service;
202
- }
201
+ deps
203
202
  }) : void 0;
204
203
  if (channelSurface) {
205
204
  const inner = deps.webhookDispatcher ?? fetchWebhookDispatcher;
206
205
  deps.webhookDispatcher = channelSurface.wrapDispatcher(inner);
207
206
  }
207
+ const mountedRoutes = channelSurface ? [...routes, ...channelSurface.routes] : routes;
208
208
  const runtime = createProtocolRuntime(deps, {
209
+ routes: mountedRoutes,
209
210
  ...channelSurface ? { handlers: channelSurface.handlers } : {},
210
211
  worker: {
211
212
  ...options.worker,
@@ -220,7 +221,6 @@ async function resolveProtocolRuntime(options, frameworkLogger) {
220
221
  enabled: options.scheduler?.enabled ?? routes.some((route) => route.group === "crons")
221
222
  }
222
223
  });
223
- runtimeRef.current = runtime;
224
224
  await runtime.service.assistants.registerGraphAssistants();
225
225
  if (options.warm) {
226
226
  await Promise.all(
@@ -262,7 +262,7 @@ async function resolveProtocolRuntime(options, frameworkLogger) {
262
262
  // Channel routes are *appended*, never declared in `skeinRoutes`: they exist only when a channel
263
263
  // is configured, which is what makes the feature undetectable to a deployment that never wanted
264
264
  // it — strictly stronger than an `http.disable_*` flag, and it needs no `RouteGroup` to express.
265
- routes: channelSurface ? [...routes, ...channelSurface.routes] : routes,
265
+ routes: mountedRoutes,
266
266
  logger
267
267
  };
268
268
  }
@@ -707,10 +707,12 @@ export {
707
707
  formatLogMeta,
708
708
  graphMapToResolver,
709
709
  joinList,
710
+ loadConfigHttpHandler,
710
711
  loadInMemoryRuntime,
711
712
  loadReloadableInMemoryRuntime,
712
713
  normalizeEmbeddableGraphs,
713
714
  readContainerMemoryLimit,
715
+ readNativeNodeRequest,
714
716
  resolveCronTickMs,
715
717
  resolveHeapPressureOptions,
716
718
  resolveIdempotency,
@@ -727,10 +729,13 @@ export {
727
729
  routeMatcherFor,
728
730
  routesFromHttpConfig,
729
731
  runFailureIdentity,
732
+ sendNativeResponse,
730
733
  sendNodeError,
731
734
  sendNodePreflight,
732
735
  sendNodeResponse,
733
736
  startHeapPressureMonitor,
734
737
  stripBasePath,
735
- toCorsOptions
738
+ toCorsOptions,
739
+ toNativeRequest,
740
+ withNativePath
736
741
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skein-js/server-kit",
3
- "version": "0.21.0",
3
+ "version": "0.23.0",
4
4
  "description": "Shared, framework-agnostic building blocks for skein-js HTTP adapters — in-memory dev runtime, LangGraph dev-state import, and langgraph.json CORS mapping.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Maina Wycliffe <wmmaina7@gmail.com>",
@@ -49,22 +49,23 @@
49
49
  "cors": "^2.8.5",
50
50
  "superjson": "^2.2.6",
51
51
  "zod": "^3.25.76",
52
- "@skein-js/agent-protocol": "0.21.0",
53
- "@skein-js/config": "0.21.0",
54
- "@skein-js/core": "0.21.0",
55
- "@skein-js/langgraph": "0.21.0",
56
- "@skein-js/storage-memory": "0.21.0"
52
+ "@skein-js/agent-protocol": "0.23.0",
53
+ "@skein-js/core": "0.23.0",
54
+ "@skein-js/config": "0.23.0",
55
+ "@skein-js/langgraph": "0.23.0",
56
+ "@skein-js/storage-memory": "0.23.0"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "@langchain/langgraph": "^1.4.0",
60
- "@skein-js/channels": "0.21.0"
60
+ "@skein-js/channels": "0.23.0"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@langchain/core": "^1.2.9",
64
64
  "@langchain/langgraph": "^1.4.0",
65
65
  "@langchain/langgraph-sdk": "^1.9.0",
66
66
  "@types/cors": "^2.8.17",
67
- "@skein-js/channels": "0.21.0"
67
+ "hono": "4.13.13",
68
+ "@skein-js/channels": "0.23.0"
68
69
  },
69
70
  "publishConfig": {
70
71
  "access": "public"
@@ -73,5 +74,8 @@
73
74
  "@skein-js/channels": {
74
75
  "optional": true
75
76
  }
77
+ },
78
+ "optionalDependencies": {
79
+ "@langchain/langgraph-ui": "1.4.2"
76
80
  }
77
81
  }
@@ -1,156 +0,0 @@
1
- // src/channels-runtime.ts
2
- import {
3
- createAuthScopedStore,
4
- createContext,
5
- createProtocolServiceFromContext
6
- } from "@skein-js/agent-protocol";
7
- import { SkeinConfigError } from "@skein-js/config";
8
- import { SkeinHttpError } from "@skein-js/core";
9
- async function resolveChannels(input) {
10
- const names = Object.keys(input.channels);
11
- if (names.length === 0) return void 0;
12
- const channels = await importChannelsPackage(names);
13
- const registry = channels.buildChannelRegistry({
14
- channels: Object.fromEntries(
15
- Object.entries(input.channels).map(([name, loaded]) => [
16
- name,
17
- {
18
- module: loaded.module,
19
- config: {
20
- assistant: loaded.assistant,
21
- ...loaded.allowedAssistants ? { allowedAssistants: loaded.allowedAssistants } : {},
22
- ...loaded.publicUrl ? { publicUrl: loaded.publicUrl } : {}
23
- }
24
- }
25
- ])
26
- ),
27
- graphIds: input.graphIds
28
- });
29
- const pipelineDeps = buildPipelineDeps(input.deps, input.service);
30
- const channelInventory = registry.names.map((routeName) => {
31
- const registered = registry.get(routeName);
32
- if (!registered) throw new Error(`Channel registry lost configured route "${routeName}".`);
33
- return {
34
- route_name: routeName,
35
- assistant: registered.config.assistant,
36
- allowed_assistants: [...registered.config.allowedAssistants ?? []],
37
- channel_name: registered.channel.name,
38
- delivery_supported: typeof registered.channel.deliver === "function"
39
- };
40
- });
41
- return {
42
- routes: [
43
- { method: "get", path: "/channels", handler: "listChannels" },
44
- ...channels.channelRouteBindings(registry.names)
45
- ],
46
- wrapDispatcher: (inner) => channels.wrapChannelDispatcher({
47
- registry,
48
- inner,
49
- logger: input.deps.logger ?? console
50
- }),
51
- handlers: {
52
- listChannels: async () => ({
53
- kind: "json",
54
- status: 200,
55
- body: { channels: channelInventory }
56
- }),
57
- handleInboundEvent: async (req) => {
58
- const name = req.url.split("?")[0].split("/channels/")[1] ?? "";
59
- const registered = registry.get(name);
60
- if (!registered) throw SkeinHttpError.notFound(`No channel named "${name}".`);
61
- return channels.handleInboundEvent(
62
- registered,
63
- {
64
- method: req.method,
65
- url: req.url,
66
- headers: req.headers,
67
- ...typeof req.body === "string" ? { text: req.body } : {}
68
- },
69
- pipelineDeps
70
- );
71
- }
72
- }
73
- };
74
- }
75
- function buildPipelineDeps(deps, service) {
76
- let requestFilters;
77
- const scopedService = (authContext) => {
78
- if (!authContext && !requestFilters) return service();
79
- const base = createContext(deps);
80
- return createProtocolServiceFromContext({
81
- ...base,
82
- ...authContext?.user ? { authUser: authContext.user } : {},
83
- ...authContext?.scopes ? { authScopes: authContext.scopes } : {},
84
- deps: requestFilters && deps.auth ? {
85
- ...base.deps,
86
- store: createAuthScopedStore(base.deps.store, deps.auth, requestFilters, "threads")
87
- } : base.deps
88
- });
89
- };
90
- return {
91
- ...deps.store.idempotency ? { idempotency: deps.store.idempotency } : {},
92
- clock: () => deps.clock ? deps.clock() : /* @__PURE__ */ new Date(),
93
- logger: deps.logger ?? console,
94
- // Straight off the run event bus — the same stream `runs.joinStream` serves, without the HTTP
95
- // connection per conversation that reaching it from outside would cost.
96
- runFrames: { subscribe: (runId) => deps.bus.subscribe(runId) },
97
- authorize: async (principal, authz) => {
98
- if (!deps.auth) return void 0;
99
- const context = {
100
- user: {
101
- identity: principal.identity,
102
- display_name: principal.identity,
103
- is_authenticated: true,
104
- permissions: principal.permissions ?? [],
105
- ...principal.metadata ?? {}
106
- },
107
- scopes: principal.permissions ?? []
108
- };
109
- const decision = await deps.auth.authorize({
110
- resource: authz.resource,
111
- action: authz.action,
112
- value: {},
113
- context
114
- });
115
- requestFilters = decision.filters;
116
- return context;
117
- },
118
- ensureThread: async (threadId, metadata, authContext) => {
119
- await scopedService(authContext).threads.create({
120
- thread_id: threadId,
121
- metadata,
122
- ifExists: "do_nothing"
123
- });
124
- },
125
- threadStatus: async (threadId) => {
126
- const thread = await service().threads.get(threadId).catch(() => null);
127
- return thread?.status ?? null;
128
- },
129
- createRun: async (input) => {
130
- const run = await scopedService(input.authContext).runs.createBackground(input.threadId, {
131
- assistant_id: input.assistantId,
132
- ...input.input !== void 0 ? { input: input.input } : {},
133
- ...input.command ? { command: input.command } : {},
134
- ...input.ifThreadStatus ? { if_thread_status: [...input.ifThreadStatus] } : {},
135
- ...input.multitaskStrategy ? { multitask_strategy: input.multitaskStrategy } : {},
136
- ...input.metadata ? { metadata: input.metadata } : {},
137
- ...input.webhook ? { webhook: input.webhook } : {},
138
- stream_mode: [...input.streamMode]
139
- });
140
- return { runId: run.run_id };
141
- }
142
- };
143
- }
144
- async function importChannelsPackage(names) {
145
- try {
146
- return await import("@skein-js/channels");
147
- } catch (cause) {
148
- throw new SkeinConfigError(
149
- `skein.channels configures ${names.join(", ")}, but @skein-js/channels is not installed. Add it as a dependency.`,
150
- { cause }
151
- );
152
- }
153
- }
154
- export {
155
- resolveChannels
156
- };