@skein-js/agent-protocol 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -102,7 +102,10 @@ Graph **state, history, and interrupt/resume are 100% LangGraph-native** via the
102
102
 
103
103
  - **Entry points:** `createProtocolRuntime(deps, options?)` → `{ service, handlers, worker }`;
104
104
  `createProtocolService` / `createProtocolServiceFromContext`; `createProtocolHandlers`; `createContext`;
105
- `createRunWorker(ctx, options?)` (`RunWorkerOptions`: `maxConcurrency`, `shutdownGraceMs`).
105
+ `createRunWorker(ctx, options?)` (`RunWorkerOptions`: `maxConcurrency` — queued runs at once,
106
+ default `DEFAULT_RUN_CONCURRENCY` (10, matching the LangGraph CLI) — and `shutdownGraceMs`). The
107
+ adapters surface this as `worker.maxConcurrency` and also read `SKEIN_RUN_CONCURRENCY` /
108
+ `N_JOBS_PER_WORKER`; see [runs-and-redis.md](../../docs/runs-and-redis.md#run-concurrency).
106
109
  - **Service surface** (`runtime.service`): `assistants` (`registerGraphAssistants`, `get`, `list`,
107
110
  `search`, `schemas`), `threads` (`create`/`get`/`list`/`patch`/`delete`/`history`/`getState`),
108
111
  `threadStream` (`stream` / `joinStream` / `command` — HIL resume, requires status `interrupted`),
package/dist/index.d.ts CHANGED
@@ -374,6 +374,12 @@ interface ProtocolRequest {
374
374
  query: Record<string, string | string[] | undefined>;
375
375
  body: unknown;
376
376
  headers: Record<string, string | undefined>;
377
+ /**
378
+ * Aborts when the client goes away, so a handler can stop work the caller will never read.
379
+ * Optional: an adapter that can't observe disconnects simply omits it. Currently honored by the
380
+ * single-graph invoke surface; protocol runs carry their own cancellation through the run row.
381
+ */
382
+ signal?: AbortSignal;
377
383
  }
378
384
  /** A normalized response an adapter serializes back onto its framework response. */
379
385
  type ProtocolResponse = {
@@ -430,8 +436,19 @@ interface ProtocolHandlers {
430
436
  }
431
437
  declare function createProtocolHandlers(service: ProtocolService): ProtocolHandlers;
432
438
 
439
+ /**
440
+ * Queued runs one worker executes at once when nothing is configured. Matches the LangGraph CLI's
441
+ * `--n-jobs-per-worker` default, so a project moving off `langgraph dev` keeps its throughput.
442
+ */
443
+ declare const DEFAULT_RUN_CONCURRENCY = 10;
433
444
  interface RunWorkerOptions {
434
- /** Max runs executing at once. Default 1 — strict per-thread serialization in dev. */
445
+ /**
446
+ * Max queued runs this worker executes at once. Default {@link DEFAULT_RUN_CONCURRENCY}.
447
+ * Per-thread ordering does not depend on this: `startRunExecution` serializes by `thread_id` at
448
+ * every value. The trade-off is head-of-line blocking — a run waiting on a busy thread's lock
449
+ * still holds a slot, so a burst of `multitask_strategy: "enqueue"` runs on one thread can occupy
450
+ * the worker. See docs/runs-and-redis.md.
451
+ */
435
452
  maxConcurrency?: number;
436
453
  /** How long `stop()` waits for in-flight runs before aborting them (ms). Default 5000. */
437
454
  shutdownGraceMs?: number;
@@ -446,7 +463,7 @@ declare function createRunWorker(ctx: ProtocolContext, options?: RunWorkerOption
446
463
 
447
464
  /** Options for {@link createProtocolRuntime}. */
448
465
  interface ProtocolRuntimeOptions {
449
- /** Tuning for the background run worker (concurrency, poll interval). */
466
+ /** Tuning for the background run worker (max concurrency, shutdown grace period). */
450
467
  worker?: RunWorkerOptions;
451
468
  }
452
469
  /** The wired engine: the service, the transport-neutral handler table, and the background worker. */
@@ -466,11 +483,16 @@ interface ProtocolRuntime {
466
483
  declare function createProtocolRuntime(deps: ProtocolDeps, options?: ProtocolRuntimeOptions): ProtocolRuntime;
467
484
 
468
485
  type HttpMethod = "get" | "post" | "put" | "patch" | "delete";
469
- interface RouteBinding {
486
+ /**
487
+ * One route. `HandlerName` defaults to a member of the protocol handler table; a surface mounted
488
+ * outside that table (the single-graph invoke endpoint) parameterizes it with its own handler names,
489
+ * so `handler` never has to lie about being a `keyof ProtocolHandlers` an adapter could look up.
490
+ */
491
+ interface RouteBinding<HandlerName = keyof ProtocolHandlers> {
470
492
  method: HttpMethod;
471
493
  /** Path with `:param` placeholders, e.g. `/threads/:thread_id/runs/stream`. */
472
494
  path: string;
473
- handler: keyof ProtocolHandlers;
495
+ handler: HandlerName;
474
496
  /**
475
497
  * Fold the path `thread_id` into the request body before dispatch. The SDK addresses a
476
498
  * thread-scoped run by its path (`POST /threads/{id}/runs/stream`) while carrying only
@@ -493,18 +515,50 @@ declare function copyThreadIdIntoBody(request: ProtocolRequest): ProtocolRequest
493
515
  */
494
516
  declare const foldThreadId: typeof copyThreadIdIntoBody;
495
517
  /** A resolved route: the matched binding plus the path params extracted from the URL. */
496
- interface RouteMatch {
497
- binding: RouteBinding;
518
+ interface RouteMatch<HandlerName = keyof ProtocolHandlers> {
519
+ binding: RouteBinding<HandlerName>;
498
520
  params: Record<string, string>;
499
521
  }
522
+ /** Matches a `method` + `pathname` against a fixed route table. Built by {@link createRouteMatcher}. */
523
+ type RouteMatcher<HandlerName = keyof ProtocolHandlers> = (method: string, pathname: string) => RouteMatch<HandlerName> | undefined;
524
+ /**
525
+ * Build a matcher over any route table — the protocol's own, or the simplified invoke surface's.
526
+ * Each `:param` path is compiled to a named-group regex once, anchored (`^…$`) so a literal segment
527
+ * can't be matched as a param value. The returned matcher iterates in table order, which is
528
+ * most-specific-first, so a literal segment wins over a `:param`.
529
+ */
530
+ declare function createRouteMatcher<HandlerName>(bindings: readonly RouteBinding<HandlerName>[]): RouteMatcher<HandlerName>;
531
+ /**
532
+ * Match a `method` + `pathname` (no query string) against the Agent Protocol route table. This is
533
+ * what adapters that dispatch from a catch-all route (NestJS middleware, Next.js route handlers) use
534
+ * in place of a framework router; Express/Fastify bind `skeinRoutes` to their native router instead.
535
+ */
536
+ declare const matchSkeinRoute: RouteMatcher;
537
+
538
+ /** The default path prefix the invoke endpoint mounts under. */
539
+ declare const DEFAULT_INVOKE_PREFIX = "/invoke";
540
+ /** Options for {@link createGraphInvokeHandler}. */
541
+ interface GraphInvokeOptions {
542
+ /**
543
+ * Stream modes used when the caller opts into SSE, overridable per request via `?stream_mode=`.
544
+ * Defaults to `"values"` — each frame is the full state after a step, ending at the same value the
545
+ * JSON response would have returned.
546
+ */
547
+ streamMode?: StreamMode | StreamMode[];
548
+ }
549
+ /** The single handler name this surface binds — deliberately not a `keyof ProtocolHandlers`. */
550
+ type GraphInvokeHandlerName = "invokeGraph";
551
+ /**
552
+ * The route table for the invoke surface — one POST, shaped like {@link skeinRoutes} so catch-all
553
+ * adapters (NestJS, Next.js) can match it the same way and native routers can bind it directly.
554
+ * Parameterized with its own handler name, so nothing can mistake it for a protocol-table lookup key.
555
+ */
556
+ declare function graphInvokeRoutes(prefix?: string): RouteBinding<GraphInvokeHandlerName>[];
500
557
  /**
501
- * Match a `method` + `pathname` (no query string) against the route table, returning the binding and
502
- * extracted path params, or `undefined` when nothing matches. Iterates in table order, which is
503
- * most-specific-first, so a literal segment wins over a `:param`. This is what adapters that dispatch
504
- * from a catch-all route (NestJS middleware, Next.js route handlers) use in place of a framework
505
- * router; Express/Fastify bind `skeinRoutes` to their native router instead.
558
+ * Build the handler for `POST <prefix>/:graph_id`. Returns a {@link ProtocolHandler}, so every
559
+ * adapter serializes it through the same JSON/SSE path the protocol routes already use.
506
560
  */
507
- declare function matchSkeinRoute(method: string, pathname: string): RouteMatch | undefined;
561
+ declare function createGraphInvokeHandler(deps: ProtocolDeps, options?: GraphInvokeOptions): ProtocolHandler;
508
562
 
509
563
  type LangGraphSearchItem = Item$1 & {
510
564
  score?: number;
@@ -559,4 +613,4 @@ declare function toSseEvents(frames: AsyncIterable<RunFrame>, finalStatus: () =>
559
613
  */
560
614
  declare function parseAfterSeq(lastEventId: string | undefined): number;
561
615
 
562
- export { type AssistantService, type Clock, type CommandInput, type CompiledGraphFactory, type CreateAssistantInput, type CreateRunInput, type CreateThreadInput, type DeleteAssistantOptions, type DrawGraphOptions, type GraphResolver, type GraphSchemas, type HistoryOptions, type HttpMethod, type Logger, type PatchThreadInput, type ProtocolContext, type ProtocolDeps, type ProtocolHandler, type ProtocolHandlers, type ProtocolRequest, type ProtocolResponse, type ProtocolRuntime, type ProtocolRuntimeOptions, type ProtocolService, type ResolvedGraph, type RouteBinding, type RouteMatch, type RunService, type RunWorker, type RunWorkerOptions, SSE_HEADERS, SkeinBaseStore, type StartedStream, type StoreService, type SubgraphsOptions, type ThreadService, type ThreadStreamInput, type ThreadStreamService, buildProtocolService, copyThreadIdIntoBody, createContext, createProtocolHandlers, createProtocolRuntime, createProtocolService, createProtocolServiceFromContext, createRunWorker, encodeFrame, encodeTerminal, foldThreadId, matchSkeinRoute, parseAfterSeq, skeinRoutes, toSseEvents };
616
+ export { type AssistantService, type Clock, type CommandInput, type CompiledGraphFactory, type CreateAssistantInput, type CreateRunInput, type CreateThreadInput, DEFAULT_INVOKE_PREFIX, DEFAULT_RUN_CONCURRENCY, type DeleteAssistantOptions, type DrawGraphOptions, type GraphInvokeHandlerName, type GraphInvokeOptions, type GraphResolver, type GraphSchemas, type HistoryOptions, type HttpMethod, type Logger, type PatchThreadInput, type ProtocolContext, type ProtocolDeps, type ProtocolHandler, type ProtocolHandlers, type ProtocolRequest, type ProtocolResponse, type ProtocolRuntime, type ProtocolRuntimeOptions, type ProtocolService, type ResolvedGraph, type RouteBinding, type RouteMatch, type RouteMatcher, type RunService, type RunWorker, type RunWorkerOptions, SSE_HEADERS, SkeinBaseStore, type StartedStream, type StoreService, type SubgraphsOptions, type ThreadService, type ThreadStreamInput, type ThreadStreamService, buildProtocolService, copyThreadIdIntoBody, createContext, createGraphInvokeHandler, createProtocolHandlers, createProtocolRuntime, createProtocolService, createProtocolServiceFromContext, createRouteMatcher, createRunWorker, encodeFrame, encodeTerminal, foldThreadId, graphInvokeRoutes, matchSkeinRoute, parseAfterSeq, skeinRoutes, toSseEvents };
package/dist/index.js CHANGED
@@ -758,50 +758,6 @@ import {
758
758
  SkeinHttpError as SkeinHttpError4
759
759
  } from "@skein-js/core";
760
760
 
761
- // src/normalize-error.ts
762
- import { isSkeinHttpError as isSkeinHttpError2, SkeinHttpError as SkeinHttpError3 } from "@skein-js/core";
763
- function serializeError(error) {
764
- if (error instanceof Error) {
765
- return { name: error.name, message: error.message };
766
- }
767
- return { name: "Error", message: String(error) };
768
- }
769
-
770
- // src/sse/run-frame-stream.ts
771
- var STREAM_MODES = [
772
- "values",
773
- "updates",
774
- "messages",
775
- "messages-tuple",
776
- "custom",
777
- "events",
778
- "debug"
779
- ];
780
- function isStreamMode(value) {
781
- return typeof value === "string" && STREAM_MODES.includes(value);
782
- }
783
- function chunkToFrameBody(chunk) {
784
- if (Array.isArray(chunk) && chunk.length === 2 && isStreamMode(chunk[0])) {
785
- return { event: chunk[0], data: chunk[1] };
786
- }
787
- return { event: "updates", data: chunk };
788
- }
789
- function toRunFrame(seq, body) {
790
- return { seq, event: body.event, data: body.data };
791
- }
792
- function streamEventToFrameBody(event, runId, graphModes) {
793
- if (event.tags?.includes("langsmith:hidden")) return null;
794
- if (event.event === "on_chain_stream" && event.run_id === runId) {
795
- const chunk = event.data?.chunk;
796
- if (Array.isArray(chunk) && chunk.length === 2 && isStreamMode(chunk[0])) {
797
- const mode = chunk[0];
798
- return graphModes.includes(mode) ? { event: mode, data: chunk[1] } : null;
799
- }
800
- return null;
801
- }
802
- return { event: "events", data: event };
803
- }
804
-
805
761
  // src/store/skein-base-store.ts
806
762
  import {
807
763
  BaseStore
@@ -878,6 +834,60 @@ var SkeinBaseStore = class extends BaseStore {
878
834
  }
879
835
  };
880
836
 
837
+ // src/graphs/resolve-compiled-graph.ts
838
+ async function resolveCompiledGraph(graphs, graphId, attachments) {
839
+ const resolved = await graphs.load(graphId);
840
+ const shared = typeof resolved === "function" ? await resolved({ configurable: attachments.configurable }) : resolved;
841
+ const graph = Object.create(shared);
842
+ graph.checkpointer = attachments.checkpointer;
843
+ graph.store = new SkeinBaseStore(attachments.store);
844
+ return graph;
845
+ }
846
+
847
+ // src/normalize-error.ts
848
+ import { isSkeinHttpError as isSkeinHttpError2, SkeinHttpError as SkeinHttpError3 } from "@skein-js/core";
849
+ function serializeError(error) {
850
+ if (error instanceof Error) {
851
+ return { name: error.name, message: error.message };
852
+ }
853
+ return { name: "Error", message: String(error) };
854
+ }
855
+
856
+ // src/sse/run-frame-stream.ts
857
+ var STREAM_MODES = [
858
+ "values",
859
+ "updates",
860
+ "messages",
861
+ "messages-tuple",
862
+ "custom",
863
+ "events",
864
+ "debug"
865
+ ];
866
+ function isStreamMode(value) {
867
+ return typeof value === "string" && STREAM_MODES.includes(value);
868
+ }
869
+ function chunkToFrameBody(chunk) {
870
+ if (Array.isArray(chunk) && chunk.length === 2 && isStreamMode(chunk[0])) {
871
+ return { event: chunk[0], data: chunk[1] };
872
+ }
873
+ return { event: "updates", data: chunk };
874
+ }
875
+ function toRunFrame(seq, body) {
876
+ return { seq, event: body.event, data: body.data };
877
+ }
878
+ function streamEventToFrameBody(event, runId, graphModes) {
879
+ if (event.tags?.includes("langsmith:hidden")) return null;
880
+ if (event.event === "on_chain_stream" && event.run_id === runId) {
881
+ const chunk = event.data?.chunk;
882
+ if (Array.isArray(chunk) && chunk.length === 2 && isStreamMode(chunk[0])) {
883
+ const mode = chunk[0];
884
+ return graphModes.includes(mode) ? { event: mode, data: chunk[1] } : null;
885
+ }
886
+ return null;
887
+ }
888
+ return { event: "events", data: event };
889
+ }
890
+
881
891
  // src/runs/run-status.ts
882
892
  import { isTerminalRunStatus } from "@skein-js/core";
883
893
  function threadStatusForRun(status) {
@@ -1125,16 +1135,11 @@ function abortedStatus(reason) {
1125
1135
  return "cancelled";
1126
1136
  }
1127
1137
  async function resolveGraph(deps, graphId, kwargs) {
1128
- const resolved = await deps.graphs.load(graphId);
1129
- const graph = typeof resolved === "function" ? (
1130
- // Expose the authenticated caller to a graph *factory* too, so a factory that branches on the
1131
- // principal sees the same `langgraph_auth_user` a node reads — sanitized identically, so a
1132
- // client can't spoof it via its own configurable.
1133
- await resolved({ configurable: toFactoryConfigurable(kwargs) })
1134
- ) : resolved;
1135
- graph.checkpointer = deps.checkpointer;
1136
- graph.store = new SkeinBaseStore(deps.store.store);
1137
- return graph;
1138
+ return resolveCompiledGraph(deps.graphs, graphId, {
1139
+ configurable: toFactoryConfigurable(kwargs),
1140
+ checkpointer: deps.checkpointer,
1141
+ store: deps.store.store
1142
+ });
1138
1143
  }
1139
1144
  async function finalizeRun(deps, runId, status) {
1140
1145
  const fresh = await deps.store.runs.get(runId);
@@ -1960,27 +1965,29 @@ function synthesizeRequest(req) {
1960
1965
  });
1961
1966
  }
1962
1967
 
1963
- // src/auth/authorizing-handlers.ts
1968
+ // src/auth/authenticate-request.ts
1964
1969
  var STUDIO_USER = {
1965
1970
  identity: "langgraph-studio-user",
1966
1971
  display_name: "langgraph-studio-user",
1967
1972
  is_authenticated: true,
1968
1973
  permissions: []
1969
1974
  };
1975
+ async function resolveAuthContext(engine, req) {
1976
+ if (!engine.studioAuthDisabled && req.headers["x-auth-scheme"] === "langsmith") {
1977
+ return { user: STUDIO_USER, scopes: [] };
1978
+ }
1979
+ return engine.authenticate(synthesizeRequest(req));
1980
+ }
1981
+
1982
+ // src/auth/authorizing-handlers.ts
1970
1983
  function createAuthorizingHandlers(context, engine) {
1971
1984
  const baseHandlers = createProtocolHandlers(createProtocolServiceFromContext(context));
1972
1985
  const names = Object.keys(ROUTE_AUTHZ);
1973
- const resolveAuthContext = async (req) => {
1974
- if (!engine.studioAuthDisabled && req.headers["x-auth-scheme"] === "langsmith") {
1975
- return { user: STUDIO_USER, scopes: [] };
1976
- }
1977
- return engine.authenticate(synthesizeRequest(req));
1978
- };
1979
1986
  const wrapped = {};
1980
1987
  for (const name of names) {
1981
1988
  const route = ROUTE_AUTHZ[name];
1982
1989
  wrapped[name] = async (req) => {
1983
- const authContext = await resolveAuthContext(req);
1990
+ const authContext = await resolveAuthContext(engine, req);
1984
1991
  const { filters } = await engine.authorize({
1985
1992
  resource: route.resource,
1986
1993
  action: route.action,
@@ -2007,6 +2014,7 @@ function createAuthorizingHandlers(context, engine) {
2007
2014
  import {
2008
2015
  isTerminalRunStatus as isTerminalRunStatus6
2009
2016
  } from "@skein-js/core";
2017
+ var DEFAULT_RUN_CONCURRENCY = 10;
2010
2018
  var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2011
2019
  function logRunLifecycle(logger, run, status, startedAt, endedAt) {
2012
2020
  logger.info(status === "success" ? "Background run succeeded" : `Background run ${status}`, {
@@ -2021,7 +2029,7 @@ function logRunLifecycle(logger, run, status, startedAt, endedAt) {
2021
2029
  }
2022
2030
  function createRunWorker(ctx, options = {}) {
2023
2031
  const { deps, control } = ctx;
2024
- const maxConcurrency = options.maxConcurrency ?? 1;
2032
+ const maxConcurrency = options.maxConcurrency ?? DEFAULT_RUN_CONCURRENCY;
2025
2033
  const shutdownGraceMs = options.shutdownGraceMs ?? 5e3;
2026
2034
  const inFlight = /* @__PURE__ */ new Set();
2027
2035
  const process = async (queued) => {
@@ -2154,19 +2162,22 @@ function copyThreadIdIntoBody(request) {
2154
2162
  return { ...request, body: { ...base, thread_id: threadId } };
2155
2163
  }
2156
2164
  var foldThreadId = copyThreadIdIntoBody;
2157
- var compiledRoutes = skeinRoutes.map((binding) => ({
2158
- binding,
2159
- regex: new RegExp(`^${binding.path.replace(/:(\w+)/g, "(?<$1>[^/]+)")}$`)
2160
- }));
2161
- function matchSkeinRoute(method, pathname) {
2162
- const wanted = method.toLowerCase();
2163
- for (const { binding, regex } of compiledRoutes) {
2164
- if (binding.method !== wanted) continue;
2165
- const match = regex.exec(pathname);
2166
- if (match) return { binding, params: decodeParams(match.groups) };
2167
- }
2168
- return void 0;
2165
+ function createRouteMatcher(bindings) {
2166
+ const compiled = bindings.map((binding) => ({
2167
+ binding,
2168
+ regex: new RegExp(`^${binding.path.replace(/:(\w+)/g, "(?<$1>[^/]+)")}$`)
2169
+ }));
2170
+ return (method, pathname) => {
2171
+ const wanted = method.toLowerCase();
2172
+ for (const { binding, regex } of compiled) {
2173
+ if (binding.method !== wanted) continue;
2174
+ const match = regex.exec(pathname);
2175
+ if (match) return { binding, params: decodeParams(match.groups) };
2176
+ }
2177
+ return void 0;
2178
+ };
2169
2179
  }
2180
+ var matchSkeinRoute = createRouteMatcher(skeinRoutes);
2170
2181
  function decodeParams(groups) {
2171
2182
  const params = {};
2172
2183
  for (const [key, value] of Object.entries(groups ?? {})) {
@@ -2178,20 +2189,150 @@ function decodeParams(groups) {
2178
2189
  }
2179
2190
  return params;
2180
2191
  }
2192
+
2193
+ // src/invoke/graph-invoke.ts
2194
+ import { randomUUID } from "crypto";
2195
+ import { MemorySaver } from "@langchain/langgraph";
2196
+ import {
2197
+ SkeinHttpError as SkeinHttpError10
2198
+ } from "@skein-js/core";
2199
+ import { z as z2 } from "zod";
2200
+ var DEFAULT_INVOKE_PREFIX = "/invoke";
2201
+ function graphInvokeRoutes(prefix = DEFAULT_INVOKE_PREFIX) {
2202
+ return [{ method: "post", path: `${normalizePrefix(prefix)}/:graph_id`, handler: "invokeGraph" }];
2203
+ }
2204
+ function normalizePrefix(prefix) {
2205
+ const trimmed = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
2206
+ return trimmed === "/" ? "" : trimmed;
2207
+ }
2208
+ function wantsEventStream(req) {
2209
+ return req.headers["accept"]?.includes("text/event-stream") ?? false;
2210
+ }
2211
+ function createRunSignal(timeoutMs, callerSignal) {
2212
+ if (timeoutMs === void 0 && !callerSignal) return { signal: void 0, dispose: () => {
2213
+ } };
2214
+ const controller = new AbortController();
2215
+ const timer = timeoutMs === void 0 ? void 0 : setTimeout(() => controller.abort(new Error(`Run exceeded ${timeoutMs}ms`)), timeoutMs);
2216
+ const onCallerAbort = () => controller.abort(callerSignal?.reason);
2217
+ if (callerSignal?.aborted) onCallerAbort();
2218
+ else callerSignal?.addEventListener("abort", onCallerAbort);
2219
+ return {
2220
+ signal: controller.signal,
2221
+ dispose: () => {
2222
+ if (timer !== void 0) clearTimeout(timer);
2223
+ callerSignal?.removeEventListener("abort", onCallerAbort);
2224
+ }
2225
+ };
2226
+ }
2227
+ var INVOKE_STREAM_MODES = [
2228
+ "values",
2229
+ "updates",
2230
+ "messages",
2231
+ "messages-tuple",
2232
+ "custom",
2233
+ "debug"
2234
+ ];
2235
+ var streamModeQuerySchema = z2.array(z2.enum(INVOKE_STREAM_MODES)).min(1, "expected at least one stream mode");
2236
+ function streamModeFromQuery(value) {
2237
+ const raw = Array.isArray(value) ? value : value === void 0 ? [] : value.split(",");
2238
+ const modes = raw.map((mode) => mode.trim()).filter((mode) => mode.length > 0);
2239
+ if (modes.length === 0) return void 0;
2240
+ if (modes.includes("events")) {
2241
+ throw SkeinHttpError10.badRequest(
2242
+ 'Stream mode "events" is not supported on the invoke surface; use the Agent Protocol run endpoints for token-level events.'
2243
+ );
2244
+ }
2245
+ return parse(streamModeQuerySchema, modes, "stream_mode query parameter");
2246
+ }
2247
+ function createGraphInvokeHandler(deps, options = {}) {
2248
+ return async (req) => {
2249
+ const graphId = requireParam(req.params, "graph_id");
2250
+ let authContext;
2251
+ if (deps.auth) {
2252
+ authContext = await resolveAuthContext(deps.auth, req);
2253
+ await deps.auth.authorize({
2254
+ resource: "threads",
2255
+ action: "create_run",
2256
+ // `authValue` spreads the body last, and on this surface the body is arbitrary caller-supplied
2257
+ // graph input — so a body key named `graph_id` would shadow the path param and let a policy
2258
+ // authorize a different graph than the one that actually runs. Re-stamp the server-derived id
2259
+ // last so the value a policy judges is always the graph we execute.
2260
+ value: { ...authValue(req), graph_id: graphId },
2261
+ context: authContext
2262
+ });
2263
+ }
2264
+ if (!deps.graphs.ids.includes(graphId)) {
2265
+ throw SkeinHttpError10.notFound(`Graph "${graphId}" is not registered.`, {
2266
+ code: "graph_not_found"
2267
+ });
2268
+ }
2269
+ const factoryConfigurable2 = withAuthUser({}, authContext?.user, authContext?.scopes);
2270
+ const graph = await resolveCompiledGraph(deps.graphs, graphId, {
2271
+ configurable: Object.keys(factoryConfigurable2).length > 0 ? factoryConfigurable2 : void 0,
2272
+ checkpointer: new MemorySaver(),
2273
+ store: deps.store.store
2274
+ });
2275
+ const configurable = withAuthUser(
2276
+ { thread_id: randomUUID() },
2277
+ authContext?.user,
2278
+ authContext?.scopes
2279
+ );
2280
+ const input = req.body ?? {};
2281
+ const { signal, dispose } = createRunSignal(deps.runTimeoutMs, req.signal);
2282
+ if (!wantsEventStream(req)) {
2283
+ try {
2284
+ const output = await graph.invoke(input, { configurable, signal });
2285
+ return { kind: "json", status: 200, body: output };
2286
+ } finally {
2287
+ dispose();
2288
+ }
2289
+ }
2290
+ const streamMode = toGraphStreamModes(
2291
+ streamModeFromQuery(req.query["stream_mode"]) ?? options.streamMode ?? "values"
2292
+ );
2293
+ let status = "success";
2294
+ const frames = async function* () {
2295
+ let seq = 0;
2296
+ try {
2297
+ const stream = await graph.stream(input, {
2298
+ configurable,
2299
+ streamMode,
2300
+ signal
2301
+ });
2302
+ for await (const chunk of stream) {
2303
+ seq += 1;
2304
+ yield toRunFrame(seq, chunkToFrameBody(chunk));
2305
+ }
2306
+ } catch (error) {
2307
+ status = "error";
2308
+ seq += 1;
2309
+ yield { seq, event: "error", data: serializeError(error) };
2310
+ } finally {
2311
+ dispose();
2312
+ }
2313
+ };
2314
+ return { kind: "sse", status: 200, events: toSseEvents(frames(), async () => status) };
2315
+ };
2316
+ }
2181
2317
  export {
2318
+ DEFAULT_INVOKE_PREFIX,
2319
+ DEFAULT_RUN_CONCURRENCY,
2182
2320
  SSE_HEADERS,
2183
2321
  SkeinBaseStore,
2184
2322
  buildProtocolService,
2185
2323
  copyThreadIdIntoBody,
2186
2324
  createContext,
2325
+ createGraphInvokeHandler,
2187
2326
  createProtocolHandlers,
2188
2327
  createProtocolRuntime,
2189
2328
  createProtocolService,
2190
2329
  createProtocolServiceFromContext,
2330
+ createRouteMatcher,
2191
2331
  createRunWorker,
2192
2332
  encodeFrame,
2193
2333
  encodeTerminal,
2194
2334
  foldThreadId,
2335
+ graphInvokeRoutes,
2195
2336
  matchSkeinRoute,
2196
2337
  parseAfterSeq,
2197
2338
  skeinRoutes,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skein-js/agent-protocol",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "description": "Framework-agnostic Agent Protocol engine for LangGraph.js — run engine, handlers, and SSE, driven entirely by injected dependencies.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Maina Wycliffe <wmmaina7@gmail.com>",
@@ -44,14 +44,14 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "zod": "^3.25.76",
47
- "@skein-js/core": "0.8.0"
47
+ "@skein-js/core": "0.9.1"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "@langchain/langgraph": "^1.4.0",
51
51
  "@langchain/langgraph-sdk": "^1.9.0"
52
52
  },
53
53
  "devDependencies": {
54
- "@skein-js/storage-memory": "0.8.0"
54
+ "@skein-js/storage-memory": "0.9.1"
55
55
  },
56
56
  "publishConfig": {
57
57
  "access": "public"