@skein-js/agent-protocol 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { GraphSchema, SkeinStore, RunQueue, RunEventBus, AuthEngine, AuthUser, Metadata, Thread, ThreadSearchQuery, ThreadState, Assistant, AssistantSearchQuery, AssistantCreate, AssistantUpdate, AssistantVersionsQuery, AssistantVersion, AssistantGraph, Config, StreamMode, MultitaskStrategy, RunFrame, DefaultValues, Run, RunStatus, StorePutOptions, Item, StoreSearchQuery, SearchItem, StoreRepo } from '@skein-js/core';
1
+ import { GraphSchema, SkeinStore, RunQueue, RunEventBus, AuthEngine, AuthUser, Metadata, Thread, ThreadSearchQuery, ThreadState, Checkpoint, Assistant, AssistantSearchQuery, AssistantCreate, AssistantUpdate, AssistantVersionsQuery, AssistantVersion, AssistantGraph, Config, StreamMode, MultitaskStrategy, RunFrame, DefaultValues, Run, RunStatus, StorePutOptions, Item, StoreSearchQuery, SearchItem, StoreRepo } from '@skein-js/core';
2
2
  import { CompiledGraph, BaseCheckpointSaver, BaseStore, Item as Item$1, Operation, OperationResults } from '@langchain/langgraph';
3
3
 
4
4
  /** A factory export: called (optionally with per-run config) to produce a compiled graph. */
@@ -184,6 +184,17 @@ interface PatchThreadInput {
184
184
  interface HistoryOptions {
185
185
  limit?: number;
186
186
  }
187
+ /** Body of `POST /threads/{id}/state` — a time-travel update that forks a new checkpoint. */
188
+ interface UpdateStateInput {
189
+ /** New state to write. `null`/`undefined` re-points `next` without changing values. */
190
+ values?: unknown;
191
+ /** Attribute the update as though this node produced `values` (sets up which node runs next). */
192
+ as_node?: string;
193
+ /** The checkpoint to fork from; omitted updates the thread tip. */
194
+ checkpoint_id?: string;
195
+ /** Full checkpoint pointer to fork from (alternative to `checkpoint_id`). */
196
+ checkpoint?: Record<string, unknown>;
197
+ }
187
198
  interface ThreadService {
188
199
  create(input?: CreateThreadInput): Promise<Thread>;
189
200
  get(threadId: string): Promise<Thread>;
@@ -197,6 +208,15 @@ interface ThreadService {
197
208
  history(threadId: string, options?: HistoryOptions): Promise<ThreadState[]>;
198
209
  /** The thread's current state snapshot — `GET /threads/{id}/state`, what `useStream` hydrates from. */
199
210
  getState(threadId: string): Promise<ThreadState>;
211
+ /** State at a specific checkpoint (time travel) — `GET /threads/{id}/state/{checkpoint_id}`. */
212
+ getStateAt(threadId: string, checkpointId: string): Promise<ThreadState>;
213
+ /**
214
+ * Update (fork) thread state at a checkpoint — `POST /threads/{id}/state`. Writes a new checkpoint
215
+ * via `graph.updateState`, mirrors it onto the thread row, and returns the new checkpoint pointer.
216
+ */
217
+ updateState(threadId: string, input: UpdateStateInput): Promise<{
218
+ checkpoint: Checkpoint;
219
+ }>;
200
220
  }
201
221
 
202
222
  /** `POST /assistants` — create input plus LangGraph's `if_exists` conflict policy. */
@@ -266,6 +286,8 @@ interface CreateRunInput {
266
286
  interrupt_after?: string[] | "*";
267
287
  /** Absolute `http(s)` URL POSTed with the settled run once it reaches a terminal status. */
268
288
  webhook?: string;
289
+ /** Time travel: fork this run from a prior checkpoint instead of the thread tip (server-validated). */
290
+ checkpoint_id?: string;
269
291
  }
270
292
  /** A started streaming run: its id, plus the live frame iterable to serialize as SSE. */
271
293
  interface StartedStream {
@@ -352,6 +374,12 @@ interface ProtocolRequest {
352
374
  query: Record<string, string | string[] | undefined>;
353
375
  body: unknown;
354
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;
355
383
  }
356
384
  /** A normalized response an adapter serializes back onto its framework response. */
357
385
  type ProtocolResponse = {
@@ -387,6 +415,8 @@ interface ProtocolHandlers {
387
415
  deleteThread: ProtocolHandler;
388
416
  getThreadHistory: ProtocolHandler;
389
417
  getThreadState: ProtocolHandler;
418
+ getThreadStateAtCheckpoint: ProtocolHandler;
419
+ updateThreadState: ProtocolHandler;
390
420
  createWaitRun: ProtocolHandler;
391
421
  createStreamRun: ProtocolHandler;
392
422
  createBackgroundRun: ProtocolHandler;
@@ -442,11 +472,16 @@ interface ProtocolRuntime {
442
472
  declare function createProtocolRuntime(deps: ProtocolDeps, options?: ProtocolRuntimeOptions): ProtocolRuntime;
443
473
 
444
474
  type HttpMethod = "get" | "post" | "put" | "patch" | "delete";
445
- interface RouteBinding {
475
+ /**
476
+ * One route. `HandlerName` defaults to a member of the protocol handler table; a surface mounted
477
+ * outside that table (the single-graph invoke endpoint) parameterizes it with its own handler names,
478
+ * so `handler` never has to lie about being a `keyof ProtocolHandlers` an adapter could look up.
479
+ */
480
+ interface RouteBinding<HandlerName = keyof ProtocolHandlers> {
446
481
  method: HttpMethod;
447
482
  /** Path with `:param` placeholders, e.g. `/threads/:thread_id/runs/stream`. */
448
483
  path: string;
449
- handler: keyof ProtocolHandlers;
484
+ handler: HandlerName;
450
485
  /**
451
486
  * Fold the path `thread_id` into the request body before dispatch. The SDK addresses a
452
487
  * thread-scoped run by its path (`POST /threads/{id}/runs/stream`) while carrying only
@@ -469,18 +504,50 @@ declare function copyThreadIdIntoBody(request: ProtocolRequest): ProtocolRequest
469
504
  */
470
505
  declare const foldThreadId: typeof copyThreadIdIntoBody;
471
506
  /** A resolved route: the matched binding plus the path params extracted from the URL. */
472
- interface RouteMatch {
473
- binding: RouteBinding;
507
+ interface RouteMatch<HandlerName = keyof ProtocolHandlers> {
508
+ binding: RouteBinding<HandlerName>;
474
509
  params: Record<string, string>;
475
510
  }
511
+ /** Matches a `method` + `pathname` against a fixed route table. Built by {@link createRouteMatcher}. */
512
+ type RouteMatcher<HandlerName = keyof ProtocolHandlers> = (method: string, pathname: string) => RouteMatch<HandlerName> | undefined;
513
+ /**
514
+ * Build a matcher over any route table — the protocol's own, or the simplified invoke surface's.
515
+ * Each `:param` path is compiled to a named-group regex once, anchored (`^…$`) so a literal segment
516
+ * can't be matched as a param value. The returned matcher iterates in table order, which is
517
+ * most-specific-first, so a literal segment wins over a `:param`.
518
+ */
519
+ declare function createRouteMatcher<HandlerName>(bindings: readonly RouteBinding<HandlerName>[]): RouteMatcher<HandlerName>;
520
+ /**
521
+ * Match a `method` + `pathname` (no query string) against the Agent Protocol route table. This is
522
+ * what adapters that dispatch from a catch-all route (NestJS middleware, Next.js route handlers) use
523
+ * in place of a framework router; Express/Fastify bind `skeinRoutes` to their native router instead.
524
+ */
525
+ declare const matchSkeinRoute: RouteMatcher;
526
+
527
+ /** The default path prefix the invoke endpoint mounts under. */
528
+ declare const DEFAULT_INVOKE_PREFIX = "/invoke";
529
+ /** Options for {@link createGraphInvokeHandler}. */
530
+ interface GraphInvokeOptions {
531
+ /**
532
+ * Stream modes used when the caller opts into SSE, overridable per request via `?stream_mode=`.
533
+ * Defaults to `"values"` — each frame is the full state after a step, ending at the same value the
534
+ * JSON response would have returned.
535
+ */
536
+ streamMode?: StreamMode | StreamMode[];
537
+ }
538
+ /** The single handler name this surface binds — deliberately not a `keyof ProtocolHandlers`. */
539
+ type GraphInvokeHandlerName = "invokeGraph";
540
+ /**
541
+ * The route table for the invoke surface — one POST, shaped like {@link skeinRoutes} so catch-all
542
+ * adapters (NestJS, Next.js) can match it the same way and native routers can bind it directly.
543
+ * Parameterized with its own handler name, so nothing can mistake it for a protocol-table lookup key.
544
+ */
545
+ declare function graphInvokeRoutes(prefix?: string): RouteBinding<GraphInvokeHandlerName>[];
476
546
  /**
477
- * Match a `method` + `pathname` (no query string) against the route table, returning the binding and
478
- * extracted path params, or `undefined` when nothing matches. Iterates in table order, which is
479
- * most-specific-first, so a literal segment wins over a `:param`. This is what adapters that dispatch
480
- * from a catch-all route (NestJS middleware, Next.js route handlers) use in place of a framework
481
- * router; Express/Fastify bind `skeinRoutes` to their native router instead.
547
+ * Build the handler for `POST <prefix>/:graph_id`. Returns a {@link ProtocolHandler}, so every
548
+ * adapter serializes it through the same JSON/SSE path the protocol routes already use.
482
549
  */
483
- declare function matchSkeinRoute(method: string, pathname: string): RouteMatch | undefined;
550
+ declare function createGraphInvokeHandler(deps: ProtocolDeps, options?: GraphInvokeOptions): ProtocolHandler;
484
551
 
485
552
  type LangGraphSearchItem = Item$1 & {
486
553
  score?: number;
@@ -535,4 +602,4 @@ declare function toSseEvents(frames: AsyncIterable<RunFrame>, finalStatus: () =>
535
602
  */
536
603
  declare function parseAfterSeq(lastEventId: string | undefined): number;
537
604
 
538
- 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 };
605
+ export { type AssistantService, type Clock, type CommandInput, type CompiledGraphFactory, type CreateAssistantInput, type CreateRunInput, type CreateThreadInput, DEFAULT_INVOKE_PREFIX, 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
@@ -64,6 +64,11 @@ var streamModeSchema = z.union([z.string(), z.array(z.string())]);
64
64
  var configSchema = z.record(z.unknown());
65
65
  var multitaskStrategySchema = z.enum(["reject", "interrupt", "rollback", "enqueue"]);
66
66
  var interruptWhenSchema = z.union([z.array(z.string()), z.literal("*")]);
67
+ var checkpointSchema = z.object({
68
+ checkpoint_id: z.string().optional(),
69
+ checkpoint_ns: z.string().nullish(),
70
+ checkpoint_map: z.record(z.unknown()).nullish()
71
+ });
67
72
  var runCreateSchema = z.object({
68
73
  assistant_id: z.string().min(1),
69
74
  thread_id: z.string().min(1).optional(),
@@ -77,7 +82,21 @@ var runCreateSchema = z.object({
77
82
  interrupt_before: interruptWhenSchema.optional(),
78
83
  interrupt_after: interruptWhenSchema.optional(),
79
84
  /** Run-completion webhook: an absolute `http(s)` URL POSTed the settled run when it finishes. */
80
- webhook: z.string().url().optional()
85
+ webhook: z.string().url().optional(),
86
+ /** Time travel: fork this run from a prior checkpoint instead of the thread tip. */
87
+ checkpoint_id: z.string().optional(),
88
+ /** Time travel: full checkpoint pointer to fork from (its `checkpoint_id` is what matters). */
89
+ checkpoint: checkpointSchema.optional()
90
+ }).passthrough();
91
+ var threadStateUpdateSchema = z.object({
92
+ /** New state to write. `null` re-points `next` without changing values; an array is a bulk write. */
93
+ values: z.union([z.record(z.unknown()), z.array(z.record(z.unknown()))]).nullish(),
94
+ /** Attribute the update as though this node produced `values` (sets up which node runs next). */
95
+ as_node: z.string().optional(),
96
+ /** The checkpoint to fork from; omitted updates the thread tip. */
97
+ checkpoint_id: z.string().optional(),
98
+ /** Full checkpoint pointer to fork from (alternative to `checkpoint_id`). */
99
+ checkpoint: checkpointSchema.nullish()
81
100
  }).passthrough();
82
101
  var threadStreamSchema = z.object({
83
102
  assistant_id: z.string().min(1),
@@ -321,6 +340,23 @@ function createProtocolHandlers(service) {
321
340
  return json(await service.threads.history(requireParam(req.params, "thread_id"), options));
322
341
  },
323
342
  getThreadState: async (req) => json(await service.threads.getState(requireParam(req.params, "thread_id"))),
343
+ getThreadStateAtCheckpoint: async (req) => json(
344
+ await service.threads.getStateAt(
345
+ requireParam(req.params, "thread_id"),
346
+ requireParam(req.params, "checkpoint_id")
347
+ )
348
+ ),
349
+ updateThreadState: async (req) => {
350
+ const body = parse(threadStateUpdateSchema, req.body ?? {});
351
+ return json(
352
+ await service.threads.updateState(requireParam(req.params, "thread_id"), {
353
+ values: body.values,
354
+ as_node: body.as_node,
355
+ checkpoint_id: body.checkpoint_id,
356
+ checkpoint: body.checkpoint ?? void 0
357
+ })
358
+ );
359
+ },
324
360
  // --- runs ---------------------------------------------------------------------------------
325
361
  createWaitRun: async (req) => json(await service.runs.createWait(parse(runCreateSchema, req.body))),
326
362
  createStreamRun: async (req) => {
@@ -722,50 +758,6 @@ import {
722
758
  SkeinHttpError as SkeinHttpError4
723
759
  } from "@skein-js/core";
724
760
 
725
- // src/normalize-error.ts
726
- import { isSkeinHttpError as isSkeinHttpError2, SkeinHttpError as SkeinHttpError3 } from "@skein-js/core";
727
- function serializeError(error) {
728
- if (error instanceof Error) {
729
- return { name: error.name, message: error.message };
730
- }
731
- return { name: "Error", message: String(error) };
732
- }
733
-
734
- // src/sse/run-frame-stream.ts
735
- var STREAM_MODES = [
736
- "values",
737
- "updates",
738
- "messages",
739
- "messages-tuple",
740
- "custom",
741
- "events",
742
- "debug"
743
- ];
744
- function isStreamMode(value) {
745
- return typeof value === "string" && STREAM_MODES.includes(value);
746
- }
747
- function chunkToFrameBody(chunk) {
748
- if (Array.isArray(chunk) && chunk.length === 2 && isStreamMode(chunk[0])) {
749
- return { event: chunk[0], data: chunk[1] };
750
- }
751
- return { event: "updates", data: chunk };
752
- }
753
- function toRunFrame(seq, body) {
754
- return { seq, event: body.event, data: body.data };
755
- }
756
- function streamEventToFrameBody(event, runId, graphModes) {
757
- if (event.tags?.includes("langsmith:hidden")) return null;
758
- if (event.event === "on_chain_stream" && event.run_id === runId) {
759
- const chunk = event.data?.chunk;
760
- if (Array.isArray(chunk) && chunk.length === 2 && isStreamMode(chunk[0])) {
761
- const mode = chunk[0];
762
- return graphModes.includes(mode) ? { event: mode, data: chunk[1] } : null;
763
- }
764
- return null;
765
- }
766
- return { event: "events", data: event };
767
- }
768
-
769
761
  // src/store/skein-base-store.ts
770
762
  import {
771
763
  BaseStore
@@ -842,6 +834,60 @@ var SkeinBaseStore = class extends BaseStore {
842
834
  }
843
835
  };
844
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
+
845
891
  // src/runs/run-status.ts
846
892
  import { isTerminalRunStatus } from "@skein-js/core";
847
893
  function threadStatusForRun(status) {
@@ -990,10 +1036,15 @@ function toFactoryConfigurable(kwargs) {
990
1036
  }
991
1037
  function toGraphCallOptions(kwargs, threadId, signal) {
992
1038
  const options = {
993
- // Drop server-owned keys from the client's configurable, force this thread's id, then stamp the
994
- // authenticated caller so the graph can authorize off `configurable.langgraph_auth_user`.
1039
+ // Drop server-owned keys from the client's configurable, force this thread's id, add the
1040
+ // server-owned time-travel fork target (if any) *after* sanitizing so the client can never spoof
1041
+ // it, then stamp the authenticated caller so the graph can authorize off `langgraph_auth_user`.
995
1042
  configurable: withAuthUser(
996
- { ...sanitizeConfigurable(kwargs.config?.configurable), thread_id: threadId },
1043
+ {
1044
+ ...sanitizeConfigurable(kwargs.config?.configurable),
1045
+ thread_id: threadId,
1046
+ ...kwargs.checkpoint_id !== void 0 ? { checkpoint_id: kwargs.checkpoint_id } : {}
1047
+ },
997
1048
  kwargs.auth_user,
998
1049
  kwargs.auth_scopes
999
1050
  ),
@@ -1084,16 +1135,11 @@ function abortedStatus(reason) {
1084
1135
  return "cancelled";
1085
1136
  }
1086
1137
  async function resolveGraph(deps, graphId, kwargs) {
1087
- const resolved = await deps.graphs.load(graphId);
1088
- const graph = typeof resolved === "function" ? (
1089
- // Expose the authenticated caller to a graph *factory* too, so a factory that branches on the
1090
- // principal sees the same `langgraph_auth_user` a node reads — sanitized identically, so a
1091
- // client can't spoof it via its own configurable.
1092
- await resolved({ configurable: toFactoryConfigurable(kwargs) })
1093
- ) : resolved;
1094
- graph.checkpointer = deps.checkpointer;
1095
- graph.store = new SkeinBaseStore(deps.store.store);
1096
- return graph;
1138
+ return resolveCompiledGraph(deps.graphs, graphId, {
1139
+ configurable: toFactoryConfigurable(kwargs),
1140
+ checkpointer: deps.checkpointer,
1141
+ store: deps.store.store
1142
+ });
1097
1143
  }
1098
1144
  async function finalizeRun(deps, runId, status) {
1099
1145
  const fresh = await deps.store.runs.get(runId);
@@ -1334,6 +1380,7 @@ function toKwargs(input, authUser, authScopes) {
1334
1380
  if (input.interrupt_before !== void 0) kwargs.interrupt_before = input.interrupt_before;
1335
1381
  if (input.interrupt_after !== void 0) kwargs.interrupt_after = input.interrupt_after;
1336
1382
  if (input.webhook !== void 0) kwargs.webhook = input.webhook;
1383
+ if (input.checkpoint_id !== void 0) kwargs.checkpoint_id = input.checkpoint_id;
1337
1384
  if (authUser !== void 0) {
1338
1385
  kwargs.auth_user = authUser;
1339
1386
  if (authScopes !== void 0) kwargs.auth_scopes = authScopes;
@@ -1554,13 +1601,13 @@ function createThreadService(ctx) {
1554
1601
  if (!thread) throw SkeinHttpError7.notFound(`Thread "${threadId}" not found.`);
1555
1602
  return thread;
1556
1603
  };
1557
- const readHistory = async (threadId, options) => {
1604
+ const loadThreadGraph = async (threadId) => {
1558
1605
  await requireThread(threadId);
1559
1606
  const runs = await deps.store.runs.listByThread(threadId);
1560
1607
  const latest = [...runs].sort((a, b) => b.created_at.localeCompare(a.created_at))[0];
1561
- if (!latest) return [];
1608
+ if (!latest) return void 0;
1562
1609
  const assistant = await deps.store.assistants.get(latest.assistant_id);
1563
- if (!assistant) return [];
1610
+ if (!assistant) return void 0;
1564
1611
  const resolved = await deps.graphs.load(assistant.graph_id);
1565
1612
  let graph;
1566
1613
  if (typeof resolved === "function") {
@@ -1570,6 +1617,11 @@ function createThreadService(ctx) {
1570
1617
  graph = resolved;
1571
1618
  }
1572
1619
  graph.checkpointer = deps.checkpointer;
1620
+ return graph;
1621
+ };
1622
+ const readHistory = async (threadId, options) => {
1623
+ const graph = await loadThreadGraph(threadId);
1624
+ if (!graph) return [];
1573
1625
  const states = [];
1574
1626
  const limit = options?.limit;
1575
1627
  for await (const snapshot of graph.getStateHistory({
@@ -1626,6 +1678,45 @@ function createThreadService(ctx) {
1626
1678
  async getState(threadId) {
1627
1679
  const [current] = await readHistory(threadId, { limit: 1 });
1628
1680
  return current ?? emptyThreadState(threadId);
1681
+ },
1682
+ async getStateAt(threadId, checkpointId) {
1683
+ const graph = await loadThreadGraph(threadId);
1684
+ if (!graph) return emptyThreadState(threadId);
1685
+ const snapshot = await graph.getState({
1686
+ configurable: { thread_id: threadId, checkpoint_ns: "", checkpoint_id: checkpointId }
1687
+ });
1688
+ return snapshotToThreadState(snapshot);
1689
+ },
1690
+ async updateState(threadId, input) {
1691
+ const graph = await loadThreadGraph(threadId);
1692
+ if (!graph) {
1693
+ throw SkeinHttpError7.unprocessable(`Thread "${threadId}" has no graph to update state on.`);
1694
+ }
1695
+ if (await deps.store.runs.hasActiveRun(threadId)) {
1696
+ throw SkeinHttpError7.conflict(
1697
+ `Thread "${threadId}" is busy; wait for its active run to finish before updating state.`
1698
+ );
1699
+ }
1700
+ const configurable = {
1701
+ checkpoint_ns: "",
1702
+ ...input.checkpoint ?? {},
1703
+ ...input.checkpoint_id !== void 0 ? { checkpoint_id: input.checkpoint_id } : {},
1704
+ thread_id: threadId
1705
+ };
1706
+ const nextConfig = await graph.updateState({ configurable }, input.values, input.as_node);
1707
+ const snapshot = await graph.getState({ configurable: { thread_id: threadId } });
1708
+ await deps.store.threads.update(
1709
+ threadId,
1710
+ snapshotToThreadUpdate(snapshot, runStatusForSnapshot(snapshot))
1711
+ );
1712
+ const next = nextConfig.configurable ?? {};
1713
+ const checkpoint = {
1714
+ thread_id: threadId,
1715
+ checkpoint_ns: next["checkpoint_ns"] ?? "",
1716
+ checkpoint_id: next["checkpoint_id"],
1717
+ checkpoint_map: next["checkpoint_map"]
1718
+ };
1719
+ return { checkpoint };
1629
1720
  }
1630
1721
  };
1631
1722
  }
@@ -1828,9 +1919,12 @@ var ROUTE_AUTHZ = {
1828
1919
  copyThread: { resource: "threads", action: "create" },
1829
1920
  getThread: { resource: "threads", action: "read" },
1830
1921
  getThreadState: { resource: "threads", action: "read" },
1922
+ getThreadStateAtCheckpoint: { resource: "threads", action: "read" },
1831
1923
  getThreadHistory: { resource: "threads", action: "read" },
1832
1924
  listThreads: { resource: "threads", action: "search" },
1833
1925
  patchThread: { resource: "threads", action: "update" },
1926
+ // Time-travel state update forks a checkpoint — a write, so read-only principals can't fork.
1927
+ updateThreadState: { resource: "threads", action: "update" },
1834
1928
  deleteThread: { resource: "threads", action: "delete" },
1835
1929
  // runs (authorized through the owning thread)
1836
1930
  createWaitRun: { resource: "threads", action: "create_run" },
@@ -1871,27 +1965,29 @@ function synthesizeRequest(req) {
1871
1965
  });
1872
1966
  }
1873
1967
 
1874
- // src/auth/authorizing-handlers.ts
1968
+ // src/auth/authenticate-request.ts
1875
1969
  var STUDIO_USER = {
1876
1970
  identity: "langgraph-studio-user",
1877
1971
  display_name: "langgraph-studio-user",
1878
1972
  is_authenticated: true,
1879
1973
  permissions: []
1880
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
1881
1983
  function createAuthorizingHandlers(context, engine) {
1882
1984
  const baseHandlers = createProtocolHandlers(createProtocolServiceFromContext(context));
1883
1985
  const names = Object.keys(ROUTE_AUTHZ);
1884
- const resolveAuthContext = async (req) => {
1885
- if (!engine.studioAuthDisabled && req.headers["x-auth-scheme"] === "langsmith") {
1886
- return { user: STUDIO_USER, scopes: [] };
1887
- }
1888
- return engine.authenticate(synthesizeRequest(req));
1889
- };
1890
1986
  const wrapped = {};
1891
1987
  for (const name of names) {
1892
1988
  const route = ROUTE_AUTHZ[name];
1893
1989
  wrapped[name] = async (req) => {
1894
- const authContext = await resolveAuthContext(req);
1990
+ const authContext = await resolveAuthContext(engine, req);
1895
1991
  const { filters } = await engine.authorize({
1896
1992
  resource: route.resource,
1897
1993
  action: route.action,
@@ -2018,6 +2114,12 @@ var skeinRoutes = [
2018
2114
  { method: "patch", path: "/threads/:thread_id", handler: "patchThread" },
2019
2115
  { method: "delete", path: "/threads/:thread_id", handler: "deleteThread" },
2020
2116
  { method: "get", path: "/threads/:thread_id/state", handler: "getThreadState" },
2117
+ { method: "post", path: "/threads/:thread_id/state", handler: "updateThreadState" },
2118
+ {
2119
+ method: "get",
2120
+ path: "/threads/:thread_id/state/:checkpoint_id",
2121
+ handler: "getThreadStateAtCheckpoint"
2122
+ },
2021
2123
  { method: "post", path: "/threads/:thread_id/history", handler: "getThreadHistory" },
2022
2124
  // runs — the stateless handlers are reused on the thread-scoped path with the id folded in
2023
2125
  { method: "post", path: "/runs/wait", handler: "createWaitRun" },
@@ -2059,19 +2161,22 @@ function copyThreadIdIntoBody(request) {
2059
2161
  return { ...request, body: { ...base, thread_id: threadId } };
2060
2162
  }
2061
2163
  var foldThreadId = copyThreadIdIntoBody;
2062
- var compiledRoutes = skeinRoutes.map((binding) => ({
2063
- binding,
2064
- regex: new RegExp(`^${binding.path.replace(/:(\w+)/g, "(?<$1>[^/]+)")}$`)
2065
- }));
2066
- function matchSkeinRoute(method, pathname) {
2067
- const wanted = method.toLowerCase();
2068
- for (const { binding, regex } of compiledRoutes) {
2069
- if (binding.method !== wanted) continue;
2070
- const match = regex.exec(pathname);
2071
- if (match) return { binding, params: decodeParams(match.groups) };
2072
- }
2073
- return void 0;
2164
+ function createRouteMatcher(bindings) {
2165
+ const compiled = bindings.map((binding) => ({
2166
+ binding,
2167
+ regex: new RegExp(`^${binding.path.replace(/:(\w+)/g, "(?<$1>[^/]+)")}$`)
2168
+ }));
2169
+ return (method, pathname) => {
2170
+ const wanted = method.toLowerCase();
2171
+ for (const { binding, regex } of compiled) {
2172
+ if (binding.method !== wanted) continue;
2173
+ const match = regex.exec(pathname);
2174
+ if (match) return { binding, params: decodeParams(match.groups) };
2175
+ }
2176
+ return void 0;
2177
+ };
2074
2178
  }
2179
+ var matchSkeinRoute = createRouteMatcher(skeinRoutes);
2075
2180
  function decodeParams(groups) {
2076
2181
  const params = {};
2077
2182
  for (const [key, value] of Object.entries(groups ?? {})) {
@@ -2083,20 +2188,149 @@ function decodeParams(groups) {
2083
2188
  }
2084
2189
  return params;
2085
2190
  }
2191
+
2192
+ // src/invoke/graph-invoke.ts
2193
+ import { randomUUID } from "crypto";
2194
+ import { MemorySaver } from "@langchain/langgraph";
2195
+ import {
2196
+ SkeinHttpError as SkeinHttpError10
2197
+ } from "@skein-js/core";
2198
+ import { z as z2 } from "zod";
2199
+ var DEFAULT_INVOKE_PREFIX = "/invoke";
2200
+ function graphInvokeRoutes(prefix = DEFAULT_INVOKE_PREFIX) {
2201
+ return [{ method: "post", path: `${normalizePrefix(prefix)}/:graph_id`, handler: "invokeGraph" }];
2202
+ }
2203
+ function normalizePrefix(prefix) {
2204
+ const trimmed = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
2205
+ return trimmed === "/" ? "" : trimmed;
2206
+ }
2207
+ function wantsEventStream(req) {
2208
+ return req.headers["accept"]?.includes("text/event-stream") ?? false;
2209
+ }
2210
+ function createRunSignal(timeoutMs, callerSignal) {
2211
+ if (timeoutMs === void 0 && !callerSignal) return { signal: void 0, dispose: () => {
2212
+ } };
2213
+ const controller = new AbortController();
2214
+ const timer = timeoutMs === void 0 ? void 0 : setTimeout(() => controller.abort(new Error(`Run exceeded ${timeoutMs}ms`)), timeoutMs);
2215
+ const onCallerAbort = () => controller.abort(callerSignal?.reason);
2216
+ if (callerSignal?.aborted) onCallerAbort();
2217
+ else callerSignal?.addEventListener("abort", onCallerAbort);
2218
+ return {
2219
+ signal: controller.signal,
2220
+ dispose: () => {
2221
+ if (timer !== void 0) clearTimeout(timer);
2222
+ callerSignal?.removeEventListener("abort", onCallerAbort);
2223
+ }
2224
+ };
2225
+ }
2226
+ var INVOKE_STREAM_MODES = [
2227
+ "values",
2228
+ "updates",
2229
+ "messages",
2230
+ "messages-tuple",
2231
+ "custom",
2232
+ "debug"
2233
+ ];
2234
+ var streamModeQuerySchema = z2.array(z2.enum(INVOKE_STREAM_MODES)).min(1, "expected at least one stream mode");
2235
+ function streamModeFromQuery(value) {
2236
+ const raw = Array.isArray(value) ? value : value === void 0 ? [] : value.split(",");
2237
+ const modes = raw.map((mode) => mode.trim()).filter((mode) => mode.length > 0);
2238
+ if (modes.length === 0) return void 0;
2239
+ if (modes.includes("events")) {
2240
+ throw SkeinHttpError10.badRequest(
2241
+ 'Stream mode "events" is not supported on the invoke surface; use the Agent Protocol run endpoints for token-level events.'
2242
+ );
2243
+ }
2244
+ return parse(streamModeQuerySchema, modes, "stream_mode query parameter");
2245
+ }
2246
+ function createGraphInvokeHandler(deps, options = {}) {
2247
+ return async (req) => {
2248
+ const graphId = requireParam(req.params, "graph_id");
2249
+ let authContext;
2250
+ if (deps.auth) {
2251
+ authContext = await resolveAuthContext(deps.auth, req);
2252
+ await deps.auth.authorize({
2253
+ resource: "threads",
2254
+ action: "create_run",
2255
+ // `authValue` spreads the body last, and on this surface the body is arbitrary caller-supplied
2256
+ // graph input — so a body key named `graph_id` would shadow the path param and let a policy
2257
+ // authorize a different graph than the one that actually runs. Re-stamp the server-derived id
2258
+ // last so the value a policy judges is always the graph we execute.
2259
+ value: { ...authValue(req), graph_id: graphId },
2260
+ context: authContext
2261
+ });
2262
+ }
2263
+ if (!deps.graphs.ids.includes(graphId)) {
2264
+ throw SkeinHttpError10.notFound(`Graph "${graphId}" is not registered.`, {
2265
+ code: "graph_not_found"
2266
+ });
2267
+ }
2268
+ const factoryConfigurable2 = withAuthUser({}, authContext?.user, authContext?.scopes);
2269
+ const graph = await resolveCompiledGraph(deps.graphs, graphId, {
2270
+ configurable: Object.keys(factoryConfigurable2).length > 0 ? factoryConfigurable2 : void 0,
2271
+ checkpointer: new MemorySaver(),
2272
+ store: deps.store.store
2273
+ });
2274
+ const configurable = withAuthUser(
2275
+ { thread_id: randomUUID() },
2276
+ authContext?.user,
2277
+ authContext?.scopes
2278
+ );
2279
+ const input = req.body ?? {};
2280
+ const { signal, dispose } = createRunSignal(deps.runTimeoutMs, req.signal);
2281
+ if (!wantsEventStream(req)) {
2282
+ try {
2283
+ const output = await graph.invoke(input, { configurable, signal });
2284
+ return { kind: "json", status: 200, body: output };
2285
+ } finally {
2286
+ dispose();
2287
+ }
2288
+ }
2289
+ const streamMode = toGraphStreamModes(
2290
+ streamModeFromQuery(req.query["stream_mode"]) ?? options.streamMode ?? "values"
2291
+ );
2292
+ let status = "success";
2293
+ const frames = async function* () {
2294
+ let seq = 0;
2295
+ try {
2296
+ const stream = await graph.stream(input, {
2297
+ configurable,
2298
+ streamMode,
2299
+ signal
2300
+ });
2301
+ for await (const chunk of stream) {
2302
+ seq += 1;
2303
+ yield toRunFrame(seq, chunkToFrameBody(chunk));
2304
+ }
2305
+ } catch (error) {
2306
+ status = "error";
2307
+ seq += 1;
2308
+ yield { seq, event: "error", data: serializeError(error) };
2309
+ } finally {
2310
+ dispose();
2311
+ }
2312
+ };
2313
+ return { kind: "sse", status: 200, events: toSseEvents(frames(), async () => status) };
2314
+ };
2315
+ }
2086
2316
  export {
2317
+ DEFAULT_INVOKE_PREFIX,
2087
2318
  SSE_HEADERS,
2088
2319
  SkeinBaseStore,
2089
2320
  buildProtocolService,
2090
2321
  copyThreadIdIntoBody,
2091
2322
  createContext,
2323
+ createGraphInvokeHandler,
2092
2324
  createProtocolHandlers,
2093
2325
  createProtocolRuntime,
2094
2326
  createProtocolService,
2095
2327
  createProtocolServiceFromContext,
2328
+ createRouteMatcher,
2096
2329
  createRunWorker,
2097
2330
  encodeFrame,
2098
2331
  encodeTerminal,
2099
2332
  foldThreadId,
2333
+ graphInvokeRoutes,
2100
2334
  matchSkeinRoute,
2101
2335
  parseAfterSeq,
2102
2336
  skeinRoutes,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skein-js/agent-protocol",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
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.7.0"
47
+ "@skein-js/core": "0.9.0"
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.7.0"
54
+ "@skein-js/storage-memory": "0.9.0"
55
55
  },
56
56
  "publishConfig": {
57
57
  "access": "public"