@skein-js/server-kit 0.15.0 → 0.17.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
@@ -81,7 +81,7 @@ Pass `overrides` to swap in production drivers or an auth engine while keeping t
81
81
  **`@deprecated`** alias. `graphMapToResolver` / `normalizeEmbeddableGraphs` are the lower-level
82
82
  graph→`GraphResolver` helpers.
83
83
  - **`resolveProtocolRuntime(options, frameworkLogger?): Promise<ResolvedProtocolRuntime>`** — turn a
84
- `{ config } | { deps }` bag (`SkeinRuntimeOptions`) into a live runtime (assistants seeded, worker
84
+ `{ config } | { deps, channels? }` bag (`SkeinRuntimeOptions`) into a live runtime (assistants seeded, worker
85
85
  started) — the step every adapter runs before mounting routes. `options.worker`
86
86
  (`RunWorkerOptions`) tunes the background worker; `worker.maxConcurrency` is how many queued runs
87
87
  run at once. `frameworkLogger` is the adapter's own default, applied only when the caller supplied
@@ -0,0 +1,156 @@
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
+ };
@@ -7,10 +7,10 @@ import {
7
7
  import { MemorySaver as MemorySaver2 } from "@langchain/langgraph";
8
8
  import {
9
9
  loadAuthEngine,
10
+ loadChannels,
10
11
  loadConfig
11
12
  } from "@skein-js/config";
12
- import { langGraphResolver as langGraphResolver2 } from "@skein-js/langgraph";
13
- import { MemoryRunEventBus as MemoryRunEventBus2, MemoryRunQueue as MemoryRunQueue2, MemorySkeinStore as MemorySkeinStore2 } from "@skein-js/storage-memory";
13
+ import { MemorySkeinStore as MemorySkeinStore2 } from "@skein-js/storage-memory";
14
14
 
15
15
  // src/cors-config.ts
16
16
  import {
@@ -18,7 +18,7 @@ import {
18
18
  skeinRoutes
19
19
  } from "@skein-js/agent-protocol";
20
20
  var ALWAYS_EXPOSED_HEADERS = ["content-location", "x-pagination-total", "idempotent-replay"];
21
- var ALWAYS_ALLOWED_HEADERS = ["idempotency-key"];
21
+ var ALWAYS_ALLOWED_HEADERS = ["idempotency-key", "x-api-key"];
22
22
  function toCorsOptions(config) {
23
23
  const options = {};
24
24
  const allowAll = config.allow_origins?.includes("*") ?? false;
@@ -148,6 +148,43 @@ function resolveMemoryBusLimits(explicit = {}, env = process.env) {
148
148
  };
149
149
  }
150
150
 
151
+ // src/webhooks-config.ts
152
+ var WEBHOOK_SECRET_ENV = "SKEIN_WEBHOOK_SECRET";
153
+ function resolveSecrets(raw, env) {
154
+ const fromEnv = (env[WEBHOOK_SECRET_ENV] ?? "").split(",").map((secret) => secret.trim()).filter((secret) => secret.length > 0);
155
+ if (fromEnv.length > 0) return fromEnv;
156
+ const configured = typeof raw.secret === "string" ? [raw.secret] : raw.secret ?? [];
157
+ return configured.map((secret) => secret.trim()).filter((secret) => secret.length > 0);
158
+ }
159
+ function resolveWebhooks(raw, options = {}) {
160
+ const env = options.env ?? process.env;
161
+ const secrets = resolveSecrets(raw ?? {}, env);
162
+ if (!raw && secrets.length === 0) return void 0;
163
+ const config = {};
164
+ if (secrets.length > 0) {
165
+ config.secrets = secrets;
166
+ if (!env[WEBHOOK_SECRET_ENV] && raw?.secret !== void 0) {
167
+ options.warn?.(
168
+ `skein: a webhook signing key is set in langgraph.json, which puts it in version control. Set ${WEBHOOK_SECRET_ENV} instead \u2014 it takes precedence over this value.`
169
+ );
170
+ }
171
+ }
172
+ if (!raw) return config;
173
+ const retries = {};
174
+ if (typeof raw.retries?.max_attempts === "number") retries.maxAttempts = raw.retries.max_attempts;
175
+ if (typeof raw.retries?.initial_delay_ms === "number") {
176
+ retries.initialDelayMs = raw.retries.initial_delay_ms;
177
+ }
178
+ if (Object.keys(retries).length > 0) config.retries = retries;
179
+ if (typeof raw.max_payload_bytes === "number") config.maxPayloadBytes = raw.max_payload_bytes;
180
+ if (typeof raw.retain_hours === "number") config.retainHours = raw.retain_hours;
181
+ if (Array.isArray(raw.allowed_hosts) && raw.allowed_hosts.length > 0) {
182
+ config.allowedHosts = [...raw.allowed_hosts];
183
+ }
184
+ if (typeof raw.require_https === "boolean") config.requireHttps = raw.require_https;
185
+ return Object.keys(config).length > 0 ? config : void 0;
186
+ }
187
+
151
188
  // src/in-memory-deps.ts
152
189
  function isGraphResolver(graphs) {
153
190
  const candidate = graphs;
@@ -174,6 +211,7 @@ function graphMapToResolver(graphs) {
174
211
  };
175
212
  }
176
213
  function embedInMemoryGraphs(graphs, overrides = {}) {
214
+ const webhooks = resolveWebhooks(void 0);
177
215
  return {
178
216
  // `maxPageSize` resolved for the same reason as the bus limits below: an embedded host's
179
217
  // `SKEIN_MAX_PAGE_SIZE` has to reach the driver that applies it.
@@ -191,6 +229,13 @@ function embedInMemoryGraphs(graphs, overrides = {}) {
191
229
  ephemeralCheckpointer: () => new MemorySaver(),
192
230
  // Clones a checkpoint before it is re-put under another thread id — see `ProtocolDeps.cloneCheckpoint`.
193
231
  cloneCheckpoint: cloneLangGraphCheckpoint,
232
+ // Resolved for the same reason as `maxPageSize` and the bus limits: an embedded host's env has to
233
+ // reach the code that applies it. Without this, a host that sets `SKEIN_WEBHOOK_SECRET` and
234
+ // follows the docs gets callbacks that are silently **unsigned** — a fail-open indistinguishable
235
+ // from a working setup, since an unsigned callback is delivered exactly like a signed one and only
236
+ // the receiver ever finds out. There is no `langgraph.json` on this path, so the env var is the
237
+ // only input; `overrides` still spreads last and wins.
238
+ ...webhooks ? { webhooks } : {},
194
239
  ...overrides
195
240
  };
196
241
  }
@@ -231,10 +276,14 @@ async function loadInMemoryRuntime(configPath, importModule, staticSchemas) {
231
276
  importModule,
232
277
  staticSchemas
233
278
  });
234
- const deps = embedInMemoryGraphs(langGraphResolver2(toGraphResolver(graphs)));
279
+ const deps = embedInMemoryGraphs(toGraphResolver(graphs));
235
280
  deps.auth = await loadAuthEngine(config.auth, { configDir, importModule });
236
281
  return {
237
282
  deps,
283
+ channels: await loadChannels(config.skein?.channels, {
284
+ configDir,
285
+ ...importModule ? { importModule } : {}
286
+ }),
238
287
  cors: corsFromHttpConfig(config.http),
239
288
  routes: routesFromHttpConfig(config.http)
240
289
  };
@@ -242,37 +291,45 @@ async function loadInMemoryRuntime(configPath, importModule, staticSchemas) {
242
291
  async function loadReloadableInMemoryRuntime(configPath, importModule, staticSchemas, extraDeps) {
243
292
  const first = await loadConfig({ configPath, importModule, staticSchemas });
244
293
  let current = first.graphs;
245
- const graphs = langGraphResolver2({
294
+ const graphs = {
246
295
  ids: first.graphs.ids,
247
296
  load: (graphId) => current.load(graphId),
248
297
  schemas: async (graphId) => await current.schemas(graphId)
249
- });
298
+ };
250
299
  const threadTtl = resolveThreadTtl(first.config.checkpointer?.ttl);
251
300
  const storeTtl = resolveStoreTtl(first.config.store?.ttl);
252
301
  const idempotency = resolveIdempotency(first.config.skein?.idempotency);
302
+ const webhooks = resolveWebhooks(first.config.skein?.webhooks, {
303
+ warn: (message) => console.warn(message)
304
+ });
253
305
  const store = new MemorySkeinStore2({
254
306
  maxPageSize: resolveMaxPageSize(),
255
307
  ...threadTtl ? { threadTtl } : {},
256
308
  ...storeTtl ? { ttl: storeTtl } : {}
257
309
  });
258
310
  const checkpointer = new MemorySaver2();
259
- const deps = {
311
+ const deps = embedInMemoryGraphs(graphs, {
260
312
  store,
261
- graphs,
262
- queue: new MemoryRunQueue2(),
263
- // This is the path `skein start` and `skein up` take by default, so it is the longest-lived
264
- // memory bus skein runs — the bounds have to be resolved here too, not just on the embed paths.
265
- bus: new MemoryRunEventBus2(resolveMemoryBusLimits()),
266
313
  checkpointer,
267
314
  auth: await loadAuthEngine(first.config.auth, { configDir: first.configDir, importModule }),
268
315
  // Present only when configured — its presence is what starts the thread TTL sweeper.
269
316
  ...threadTtl ? { threadTtl } : {},
270
317
  // Unlike `threadTtl`, absence here means *defaults*, not *off*: the header is honoured either way.
271
318
  ...idempotency ? { idempotency } : {},
319
+ // Resolved from this runtime's config, which `resolveWebhooks` already merges with the environment
320
+ // — so this subsumes the env-only value `embedInMemoryGraphs` computes, and overrides spread last.
321
+ ...webhooks ? { webhooks } : {},
272
322
  ...extraDeps
273
- };
323
+ });
274
324
  return {
275
325
  deps,
326
+ // Loaded here so the modules are imported once, with the same importer the graphs used — under
327
+ // `skein dev` that is the vite-backed one, which is what lets a channel be a TypeScript file in
328
+ // the user's own repo rather than something they have to build first.
329
+ channels: await loadChannels(first.config.skein?.channels, {
330
+ configDir: first.configDir,
331
+ ...importModule ? { importModule } : {}
332
+ }),
276
333
  cors: corsFromHttpConfig(first.config.http),
277
334
  routes: routesFromHttpConfig(first.config.http),
278
335
  config: first.config,
@@ -303,6 +360,7 @@ export {
303
360
  resolveIdempotency,
304
361
  resolveMaxPageSize,
305
362
  resolveMemoryBusLimits,
363
+ resolveWebhooks,
306
364
  normalizeEmbeddableGraphs,
307
365
  graphMapToResolver,
308
366
  embedInMemoryGraphs,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  loadInMemoryRuntime,
3
3
  loadReloadableInMemoryRuntime
4
- } from "./chunk-TCYGP4ZJ.js";
4
+ } from "./chunk-5Q3G665D.js";
5
5
  import "./chunk-JUBBCC7R.js";
6
6
  export {
7
7
  loadInMemoryRuntime,
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { ProtocolRuntime, RouteBinding, Logger, ProtocolDeps, RunWorkerOptions, CronSchedulerOptions, RunFailureReport, IdempotencyConfig, GraphResolver, RouteMatcher, DisabledRouteGroups, ProtocolResponse } from '@skein-js/agent-protocol';
1
+ import { ProtocolRuntime, RouteBinding, Logger, ProtocolDeps, RunWorkerOptions, CronSchedulerOptions, RunFailureReport, IdempotencyConfig, WebhookDeliveryConfig, GraphResolver, RouteMatcher, DisabledRouteGroups, ProtocolResponse } 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
- import { ModuleImporter, LanggraphJson, GraphSchemas } from '@skein-js/config';
3
+ import { LoadedChannel, ModuleImporter, LanggraphJson, GraphSchemas } from '@skein-js/config';
4
4
  import { CorsOptions } from 'cors';
5
5
  export { CorsOptions } from 'cors';
6
6
  import { MemoryRunEventBusOptions } from '@skein-js/storage-memory';
@@ -85,6 +85,8 @@ type SkeinRuntimeOptions = SkeinRuntimeCommonOptions & ({
85
85
  deps: ProtocolDeps;
86
86
  config?: never;
87
87
  importModule?: never;
88
+ /** Preloaded channel modules when another assembler consumed langgraph.json before us. */
89
+ channels?: Record<string, LoadedChannel>;
88
90
  });
89
91
  interface ResolvedProtocolRuntime {
90
92
  /** The wired runtime — assistants seeded and the background worker already started. */
@@ -114,6 +116,8 @@ interface ResolvedRuntimeDeps {
114
116
  routes: readonly RouteBinding[];
115
117
  /** The resolved logger — see {@link ResolvedProtocolRuntime.logger}. */
116
118
  logger?: Logger;
119
+ /** Channel modules named by `skein.channels`, already imported. Absent on the injected-deps path. */
120
+ channels?: Record<string, LoadedChannel>;
117
121
  }
118
122
  /**
119
123
  * Resolve the `{ config } | { deps }` seam down to a `ProtocolDeps` — injected as-is, or fresh
@@ -416,6 +420,13 @@ interface InMemoryRuntimeConfig {
416
420
  cors?: CorsOptions;
417
421
  /** The route table to mount, with any `http.disable_*` groups already removed. */
418
422
  routes: readonly RouteBinding[];
423
+ /**
424
+ * Channel modules named by `skein.channels`, already imported, keyed by name.
425
+ *
426
+ * Empty when none is configured, which is what keeps the channel routes out of the table entirely
427
+ * rather than merely disabled.
428
+ */
429
+ channels?: Record<string, LoadedChannel>;
419
430
  }
420
431
  /** Load `langgraph.json`, wiring fresh in-memory drivers and reading its `http.cors` for the adapter. */
421
432
  declare function loadInMemoryRuntime(configPath: string, importModule?: ModuleImporter, staticSchemas?: Record<string, GraphSchemas>): Promise<InMemoryRuntimeConfig>;
@@ -497,14 +508,55 @@ interface RawIdempotency {
497
508
  */
498
509
  declare function resolveIdempotency(raw: RawIdempotency | undefined): IdempotencyConfig | undefined;
499
510
 
511
+ /** The raw `skein.webhooks` block as `langgraph.json` spells it. */
512
+ interface RawWebhooks {
513
+ retries?: {
514
+ max_attempts?: number;
515
+ initial_delay_ms?: number;
516
+ };
517
+ secret?: string | string[];
518
+ max_payload_bytes?: number;
519
+ retain_hours?: number;
520
+ allowed_hosts?: string[];
521
+ require_https?: boolean;
522
+ }
523
+ /**
524
+ * Map the raw block to {@link WebhookDeliveryConfig}, or `undefined` when nothing is set.
525
+ *
526
+ * `undefined` means **defaults, not disabled**, the same distinction `resolveIdempotency` draws: a run
527
+ * that carries a `webhook` owes a callback whether or not a policy was configured. There is no
528
+ * setting that stops skein trying — only `retries.max_attempts: 1`, which asks it to try once.
529
+ */
530
+ declare function resolveWebhooks(raw: RawWebhooks | undefined, options?: {
531
+ env?: Record<string, string | undefined>;
532
+ warn?: (message: string) => void;
533
+ }): WebhookDeliveryConfig | undefined;
534
+
500
535
  type AnyCompiledGraph = CompiledGraph<any>;
501
536
  /**
502
- * A graph you can embed in code: any compiled LangGraph.js graph, or a factory that builds one per run
503
- * (called with the run's `configurable`). Keys of a graph map become graph ids.
537
+ * The surface the engine actually drives, stated structurally.
538
+ *
539
+ * `CompiledGraph` is not the right bound here even though every graph is one. LangChain's own
540
+ * `createAgent` returns a `ReactAgent`, which is graph-*like* — it streams and it has state — but is
541
+ * not structurally a `CompiledGraph`, so requiring that type rejected the blessed way to build an
542
+ * agent. The engine never needed it: `langGraphResolver` casts whatever it is to an `AgentGraph`, and
543
+ * `AgentGraph` requires exactly these two methods.
544
+ *
545
+ * Kept narrow enough to still catch a mistake — an object with neither method is not a graph — while
546
+ * accepting anything the runtime can genuinely drive.
547
+ */
548
+ interface EmbeddableAgentSurface {
549
+ stream(input: any, options?: any): any;
550
+ getState(config: any): any;
551
+ }
552
+ /**
553
+ * A graph you can embed in code: any compiled LangGraph.js graph, anything `createAgent` returns, or a
554
+ * factory that builds one per run (called with the run's `configurable`). Keys of a graph map become
555
+ * graph ids.
504
556
  */
505
- type EmbeddableGraph = AnyCompiledGraph | ((config: {
557
+ type EmbeddableGraph = AnyCompiledGraph | EmbeddableAgentSurface | ((config: {
506
558
  configurable?: Record<string, unknown>;
507
- }) => AnyCompiledGraph | Promise<AnyCompiledGraph>);
559
+ }) => AnyCompiledGraph | EmbeddableAgentSurface | Promise<AnyCompiledGraph | EmbeddableAgentSurface>);
508
560
  /**
509
561
  * Accept either a ready {@link GraphResolver} or a plain graph map and return a `GraphResolver` either
510
562
  * way — the normalization every in-code embedding helper shares (`embedInMemoryGraphs` here,
@@ -703,4 +755,4 @@ type RuntimeGlobal = typeof globalThis & {
703
755
  /** Detect Node, Bun, or Deno without importing a runtime-specific module. */
704
756
  declare function detectRuntimeCapabilities(scope?: RuntimeGlobal): RuntimeCapabilities;
705
757
 
706
- export { type ConsoleLogLevel, type ConsoleLoggerOptions, type CorsSetting, DEFAULT_HEAP_SAMPLE_MS, DEFAULT_HEAP_WARN_PERCENT, DevStateSnapshot, type EmbeddableGraph, 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 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, graphMapToResolver, joinList, loadInMemoryRuntime, loadReloadableInMemoryRuntime, normalizeEmbeddableGraphs, readContainerMemoryLimit, resolveCronTickMs, resolveHeapPressureOptions, resolveIdempotency, resolveMaxPageSize, resolveMemoryBusLimits, resolveProtocolRuntime, resolveRunConcurrency, resolveRunTimeoutMs, resolveRuntimeDeps, resolveShutdownGraceMs, resolveStoreTtl, resolveThreadTtl, routeMatcherFor, routesFromHttpConfig, runFailureIdentity, sendNodeError, sendNodePreflight, sendNodeResponse, startHeapPressureMonitor, stripBasePath, toCorsOptions };
758
+ export { type ConsoleLogLevel, type ConsoleLoggerOptions, type CorsSetting, DEFAULT_HEAP_SAMPLE_MS, DEFAULT_HEAP_WARN_PERCENT, DevStateSnapshot, type EmbeddableGraph, 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, graphMapToResolver, joinList, loadInMemoryRuntime, loadReloadableInMemoryRuntime, normalizeEmbeddableGraphs, readContainerMemoryLimit, resolveCronTickMs, resolveHeapPressureOptions, resolveIdempotency, resolveMaxPageSize, resolveMemoryBusLimits, resolveProtocolRuntime, resolveRunConcurrency, resolveRunTimeoutMs, resolveRuntimeDeps, resolveShutdownGraceMs, resolveStoreTtl, resolveThreadTtl, resolveWebhooks, routeMatcherFor, routesFromHttpConfig, runFailureIdentity, sendNodeError, sendNodePreflight, sendNodeResponse, startHeapPressureMonitor, stripBasePath, toCorsOptions };
package/dist/index.js CHANGED
@@ -15,14 +15,16 @@ import {
15
15
  resolveMemoryBusLimits,
16
16
  resolveStoreTtl,
17
17
  resolveThreadTtl,
18
+ resolveWebhooks,
18
19
  routesFromHttpConfig,
19
20
  toCorsOptions
20
- } from "./chunk-TCYGP4ZJ.js";
21
+ } from "./chunk-5Q3G665D.js";
21
22
  import "./chunk-JUBBCC7R.js";
22
23
 
23
24
  // src/resolve-runtime.ts
24
25
  import {
25
26
  createProtocolRuntime,
27
+ fetchWebhookDispatcher,
26
28
  skeinRoutes
27
29
  } from "@skein-js/agent-protocol";
28
30
 
@@ -166,22 +168,45 @@ function resolveLogger(option, injected, frameworkLogger) {
166
168
  return option ?? frameworkLogger;
167
169
  }
168
170
  async function resolveRuntimeDeps(options, frameworkLogger) {
169
- const loaded = options.deps ? { deps: options.deps, cors: void 0, routes: skeinRoutes } : await (await import("./in-memory-runtime-AOVHAXHJ.js")).loadInMemoryRuntime(options.config, options.importModule);
171
+ const loaded = options.deps ? { deps: options.deps, cors: void 0, routes: skeinRoutes, channels: options.channels } : await (await import("./in-memory-runtime-N3GPWIA5.js")).loadInMemoryRuntime(options.config, options.importModule);
170
172
  const logger = resolveLogger(options.logger, loaded.deps.logger, frameworkLogger);
171
173
  if (logger && !loaded.deps.logger) loaded.deps.logger = logger;
172
- return { deps: loaded.deps, cors: loaded.cors, routes: loaded.routes, logger };
174
+ return {
175
+ deps: loaded.deps,
176
+ cors: loaded.cors,
177
+ routes: loaded.routes,
178
+ ...loaded.channels ? { channels: loaded.channels } : {},
179
+ logger
180
+ };
173
181
  }
174
182
  async function resolveProtocolRuntime(options, frameworkLogger) {
175
183
  const {
176
184
  deps,
177
185
  cors: corsFromConfig,
178
186
  routes,
187
+ channels,
179
188
  logger
180
189
  } = await resolveRuntimeDeps(options, frameworkLogger);
181
190
  const heapPressureOptions = resolveHeapPressureOptions();
182
191
  const runTimeoutMs = resolveRunTimeoutMs(deps.runTimeoutMs);
183
192
  if (runTimeoutMs !== void 0) deps.runTimeoutMs = runTimeoutMs;
193
+ const runtimeRef = {};
194
+ const channelSurface = channels ? await (await import("./channels-runtime-JHTMXARF.js")).resolveChannels({
195
+ channels,
196
+ 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
+ }
203
+ }) : void 0;
204
+ if (channelSurface) {
205
+ const inner = deps.webhookDispatcher ?? fetchWebhookDispatcher;
206
+ deps.webhookDispatcher = channelSurface.wrapDispatcher(inner);
207
+ }
184
208
  const runtime = createProtocolRuntime(deps, {
209
+ ...channelSurface ? { handlers: channelSurface.handlers } : {},
185
210
  worker: {
186
211
  ...options.worker,
187
212
  maxConcurrency: resolveRunConcurrency(options.worker?.maxConcurrency),
@@ -195,6 +220,7 @@ async function resolveProtocolRuntime(options, frameworkLogger) {
195
220
  enabled: options.scheduler?.enabled ?? routes.some((route) => route.group === "crons")
196
221
  }
197
222
  });
223
+ runtimeRef.current = runtime;
198
224
  await runtime.service.assistants.registerGraphAssistants();
199
225
  if (options.warm) {
200
226
  await Promise.all(
@@ -230,7 +256,15 @@ async function resolveProtocolRuntime(options, frameworkLogger) {
230
256
  await runtime.worker.stop();
231
257
  }
232
258
  };
233
- return { runtime: { ...runtime, worker }, cors: corsFromConfig, routes, logger };
259
+ return {
260
+ runtime: { ...runtime, worker },
261
+ cors: corsFromConfig,
262
+ // Channel routes are *appended*, never declared in `skeinRoutes`: they exist only when a channel
263
+ // is configured, which is what makes the feature undetectable to a deployment that never wanted
264
+ // it — strictly stronger than an `http.disable_*` flag, and it needs no `RouteGroup` to express.
265
+ routes: channelSurface ? [...routes, ...channelSurface.routes] : routes,
266
+ logger
267
+ };
234
268
  }
235
269
 
236
270
  // src/log-meta.ts
@@ -529,6 +563,7 @@ function sendNodeError(error, res, logger, adapterName = "skein") {
529
563
  return;
530
564
  }
531
565
  if (isSkeinHttpError(error)) {
566
+ if (error.status >= 500) logger?.error(`Request failed: ${error.message}`, error);
532
567
  res.writeHead(error.status, { "content-type": "application/json" });
533
568
  res.end(
534
569
  JSON.stringify({
@@ -688,6 +723,7 @@ export {
688
723
  resolveShutdownGraceMs,
689
724
  resolveStoreTtl,
690
725
  resolveThreadTtl,
726
+ resolveWebhooks,
691
727
  routeMatcherFor,
692
728
  routesFromHttpConfig,
693
729
  runFailureIdentity,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skein-js/server-kit",
3
- "version": "0.15.0",
3
+ "version": "0.17.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,21 +49,29 @@
49
49
  "cors": "^2.8.5",
50
50
  "superjson": "^2.2.6",
51
51
  "zod": "^3.25.76",
52
- "@skein-js/agent-protocol": "0.15.0",
53
- "@skein-js/config": "0.15.0",
54
- "@skein-js/core": "0.15.0",
55
- "@skein-js/langgraph": "0.15.0",
56
- "@skein-js/storage-memory": "0.15.0"
52
+ "@skein-js/agent-protocol": "0.17.0",
53
+ "@skein-js/config": "0.17.0",
54
+ "@skein-js/core": "0.17.0",
55
+ "@skein-js/langgraph": "0.17.0",
56
+ "@skein-js/storage-memory": "0.17.0"
57
57
  },
58
58
  "peerDependencies": {
59
- "@langchain/langgraph": "^1.4.0"
59
+ "@langchain/langgraph": "^1.4.0",
60
+ "@skein-js/channels": "0.17.0"
60
61
  },
61
62
  "devDependencies": {
62
- "@langchain/core": "^1.2.0",
63
+ "@langchain/core": "^1.2.9",
63
64
  "@langchain/langgraph": "^1.4.0",
64
- "@types/cors": "^2.8.17"
65
+ "@langchain/langgraph-sdk": "^1.9.0",
66
+ "@types/cors": "^2.8.17",
67
+ "@skein-js/channels": "0.17.0"
65
68
  },
66
69
  "publishConfig": {
67
70
  "access": "public"
71
+ },
72
+ "peerDependenciesMeta": {
73
+ "@skein-js/channels": {
74
+ "optional": true
75
+ }
68
76
  }
69
77
  }