@skein-js/agent-protocol 0.4.0 → 0.6.3

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.
Files changed (3) hide show
  1. package/dist/index.d.ts +190 -44
  2. package/dist/index.js +686 -113
  3. package/package.json +6 -6
package/dist/index.js CHANGED
@@ -75,7 +75,9 @@ var runCreateSchema = z.object({
75
75
  metadata: z.record(z.unknown()).optional(),
76
76
  multitask_strategy: multitaskStrategySchema.optional(),
77
77
  interrupt_before: interruptWhenSchema.optional(),
78
- interrupt_after: interruptWhenSchema.optional()
78
+ interrupt_after: interruptWhenSchema.optional(),
79
+ /** Run-completion webhook: an absolute `http(s)` URL POSTed the settled run when it finishes. */
80
+ webhook: z.string().url().optional()
79
81
  }).passthrough();
80
82
  var threadStreamSchema = z.object({
81
83
  assistant_id: z.string().min(1),
@@ -111,11 +113,47 @@ var threadSearchSchema = z.object({
111
113
  sort_by: z.enum(["thread_id", "status", "created_at", "updated_at"]).optional(),
112
114
  sort_order: z.enum(["asc", "desc"]).optional()
113
115
  }).passthrough();
116
+ var assistantCreateSchema = z.object({
117
+ graph_id: z.string().min(1),
118
+ assistant_id: z.string().min(1).optional(),
119
+ name: z.string().optional(),
120
+ description: z.string().optional(),
121
+ config: configSchema.optional(),
122
+ context: z.unknown().optional(),
123
+ metadata: z.record(z.unknown()).optional(),
124
+ /** Conflict policy when `assistant_id` already exists; defaults to `raise`. */
125
+ if_exists: z.enum(["raise", "do_nothing"]).optional()
126
+ }).passthrough();
127
+ var assistantUpdateSchema = z.object({
128
+ graph_id: z.string().min(1).optional(),
129
+ name: z.string().optional(),
130
+ description: z.string().optional(),
131
+ config: configSchema.optional(),
132
+ context: z.unknown().optional(),
133
+ metadata: z.record(z.unknown()).optional()
134
+ }).passthrough();
114
135
  var assistantSearchSchema = z.object({
115
136
  graph_id: z.string().optional(),
116
- limit: z.number().int().positive().optional(),
137
+ name: z.string().optional(),
138
+ metadata: z.record(z.unknown()).optional(),
139
+ limit: z.number().int().positive().max(1e3).optional(),
140
+ offset: z.number().int().nonnegative().optional(),
141
+ sort_by: z.enum(["assistant_id", "graph_id", "name", "created_at", "updated_at"]).optional(),
142
+ sort_order: z.enum(["asc", "desc"]).optional()
143
+ }).passthrough();
144
+ var assistantCountSchema = z.object({
145
+ graph_id: z.string().optional(),
146
+ name: z.string().optional(),
147
+ metadata: z.record(z.unknown()).optional()
148
+ }).passthrough();
149
+ var assistantVersionsSchema = z.object({
150
+ metadata: z.record(z.unknown()).optional(),
151
+ limit: z.number().int().positive().max(1e3).optional(),
117
152
  offset: z.number().int().nonnegative().optional()
118
153
  }).passthrough();
154
+ var assistantSetLatestSchema = z.object({
155
+ version: z.number().int().positive()
156
+ }).passthrough();
119
157
  var storePutSchema = z.object({
120
158
  namespace: z.array(z.string()).min(1),
121
159
  key: z.string().min(1),
@@ -150,6 +188,15 @@ function positiveIntQuery(value) {
150
188
  const parsed = Number.parseInt(raw, 10);
151
189
  return Number.isInteger(parsed) && parsed > 0 ? parsed : void 0;
152
190
  }
191
+ function booleanQuery(value) {
192
+ const raw = queryValue(value);
193
+ if (raw === "true") return true;
194
+ if (raw === "false") return false;
195
+ return void 0;
196
+ }
197
+ function xrayQuery(value) {
198
+ return booleanQuery(value) ?? positiveIntQuery(value);
199
+ }
153
200
  function createProtocolHandlers(service) {
154
201
  const sse = (runId, frames) => ({
155
202
  kind: "sse",
@@ -160,8 +207,85 @@ function createProtocolHandlers(service) {
160
207
  return {
161
208
  // --- assistants ---------------------------------------------------------------------------
162
209
  getAssistant: async (req) => json(await service.assistants.get(requireParam(req.params, "assistant_id"))),
163
- searchAssistants: async (req) => json(await service.assistants.search(parse(assistantSearchSchema, req.body ?? {}))),
210
+ searchAssistants: async (req) => {
211
+ const body = parse(assistantSearchSchema, req.body ?? {});
212
+ return json(
213
+ await service.assistants.search({
214
+ graph_id: body.graph_id,
215
+ name: body.name,
216
+ metadata: body.metadata,
217
+ limit: body.limit,
218
+ offset: body.offset,
219
+ sortBy: body.sort_by,
220
+ sortOrder: body.sort_order
221
+ })
222
+ );
223
+ },
224
+ countAssistants: async (req) => {
225
+ const body = parse(assistantCountSchema, req.body ?? {});
226
+ return json(
227
+ await service.assistants.count({
228
+ graph_id: body.graph_id,
229
+ name: body.name,
230
+ metadata: body.metadata
231
+ })
232
+ );
233
+ },
164
234
  getAssistantSchemas: async (req) => json(await service.assistants.schemas(requireParam(req.params, "assistant_id"))),
235
+ createAssistant: async (req) => {
236
+ const body = parse(assistantCreateSchema, req.body);
237
+ return json(
238
+ await service.assistants.create({
239
+ graph_id: body.graph_id,
240
+ assistant_id: body.assistant_id,
241
+ name: body.name,
242
+ description: body.description,
243
+ config: body.config,
244
+ context: body.context,
245
+ metadata: body.metadata,
246
+ ifExists: body.if_exists
247
+ })
248
+ );
249
+ },
250
+ updateAssistant: async (req) => json(
251
+ await service.assistants.update(
252
+ requireParam(req.params, "assistant_id"),
253
+ parse(assistantUpdateSchema, req.body ?? {})
254
+ )
255
+ ),
256
+ deleteAssistant: async (req) => {
257
+ await service.assistants.delete(requireParam(req.params, "assistant_id"), {
258
+ deleteThreads: booleanQuery(req.query["delete_threads"]) ?? false
259
+ });
260
+ return empty();
261
+ },
262
+ listAssistantVersions: async (req) => {
263
+ const body = parse(assistantVersionsSchema, req.body ?? {});
264
+ return json(
265
+ await service.assistants.listVersions(requireParam(req.params, "assistant_id"), {
266
+ metadata: body.metadata,
267
+ limit: body.limit,
268
+ offset: body.offset
269
+ })
270
+ );
271
+ },
272
+ setAssistantLatestVersion: async (req) => {
273
+ const body = parse(assistantSetLatestSchema, req.body);
274
+ return json(
275
+ await service.assistants.setLatest(requireParam(req.params, "assistant_id"), body.version)
276
+ );
277
+ },
278
+ getAssistantGraph: async (req) => json(
279
+ await service.assistants.drawGraph(requireParam(req.params, "assistant_id"), {
280
+ xray: xrayQuery(req.query["xray"])
281
+ })
282
+ ),
283
+ getAssistantSubgraphs: async (req) => json(
284
+ await service.assistants.subgraphs(requireParam(req.params, "assistant_id"), {
285
+ namespace: queryValue(req.query["namespace"]) ?? req.params["namespace"],
286
+ recurse: booleanQuery(req.query["recurse"]) ?? false
287
+ })
288
+ ),
165
289
  // --- threads ------------------------------------------------------------------------------
166
290
  createThread: async (req) => json(await service.threads.create(parse(threadCreateSchema, req.body ?? {}))),
167
291
  getThread: async (req) => json(await service.threads.get(requireParam(req.params, "thread_id"))),
@@ -280,35 +404,133 @@ function createProtocolHandlers(service) {
280
404
  }
281
405
 
282
406
  // src/assistants/assistant-service.ts
283
- import { SkeinHttpError as SkeinHttpError2 } from "@skein-js/core";
284
- function createAssistantService(deps) {
407
+ import {
408
+ isSkeinHttpError,
409
+ SkeinHttpError as SkeinHttpError2
410
+ } from "@skein-js/core";
411
+ function factoryConfigurable(config) {
412
+ const configurable = config?.configurable;
413
+ return { configurable };
414
+ }
415
+ function createAssistantService(ctx, threads) {
416
+ const { deps } = ctx;
285
417
  const requireAssistant = async (assistantId) => {
286
418
  const assistant = await deps.store.assistants.get(assistantId);
287
419
  if (!assistant) throw SkeinHttpError2.notFound(`Assistant "${assistantId}" not found.`);
288
420
  return assistant;
289
421
  };
422
+ const requireGraph = (graphId) => {
423
+ if (!deps.graphs.ids.includes(graphId)) {
424
+ throw SkeinHttpError2.badRequest(`Unknown graph "${graphId}".`);
425
+ }
426
+ };
427
+ const deletableThreads = async (assistantId) => {
428
+ const owned = await deps.store.threads.search({ metadata: { assistant_id: assistantId } });
429
+ const engine = deps.auth;
430
+ if (!engine?.enabled || !ctx.authUser) return owned;
431
+ try {
432
+ const { filters } = await engine.authorize({
433
+ resource: "threads",
434
+ action: "delete",
435
+ value: { assistant_id: assistantId },
436
+ context: { user: ctx.authUser, scopes: ctx.authScopes ?? [] }
437
+ });
438
+ if (!filters) return owned;
439
+ return owned.filter((thread) => engine.matchesFilters(thread.metadata ?? void 0, filters));
440
+ } catch {
441
+ return [];
442
+ }
443
+ };
444
+ const compileGraph = async (assistant) => {
445
+ const resolved = await deps.graphs.load(assistant.graph_id);
446
+ return typeof resolved === "function" ? await resolved(factoryConfigurable(assistant.config)) : resolved;
447
+ };
290
448
  return {
291
449
  async registerGraphAssistants() {
292
450
  const registered = [];
293
451
  for (const graphId of deps.graphs.ids) {
294
452
  const existing = await deps.store.assistants.get(graphId);
295
- registered.push(
296
- existing ?? await deps.store.assistants.create({ graph_id: graphId, assistant_id: graphId })
297
- );
453
+ if (existing) {
454
+ registered.push(existing);
455
+ continue;
456
+ }
457
+ try {
458
+ registered.push(
459
+ await deps.store.assistants.create({ graph_id: graphId, assistant_id: graphId })
460
+ );
461
+ } catch (error) {
462
+ if (isSkeinHttpError(error) && error.status === 409) {
463
+ const now = await deps.store.assistants.get(graphId);
464
+ if (now) {
465
+ registered.push(now);
466
+ continue;
467
+ }
468
+ }
469
+ throw error;
470
+ }
298
471
  }
299
472
  return registered;
300
473
  },
301
474
  get: requireAssistant,
302
475
  list: () => deps.store.assistants.list(),
303
- async search(query) {
304
- const all = await deps.store.assistants.list();
305
- const filtered = query.graph_id ? all.filter((assistant) => assistant.graph_id === query.graph_id) : all;
306
- const offset = query.offset ?? 0;
307
- return filtered.slice(offset, query.limit === void 0 ? void 0 : offset + query.limit);
308
- },
476
+ search: (query) => deps.store.assistants.search(query),
477
+ count: (query) => deps.store.assistants.count(query),
309
478
  async schemas(assistantId) {
310
479
  const assistant = await requireAssistant(assistantId);
311
480
  return deps.graphs.schemas(assistant.graph_id);
481
+ },
482
+ async create({ ifExists, ...input }) {
483
+ requireGraph(input.graph_id);
484
+ try {
485
+ return await deps.store.assistants.create(input);
486
+ } catch (error) {
487
+ if (input.assistant_id !== void 0 && ifExists === "do_nothing" && isSkeinHttpError(error) && error.status === 409) {
488
+ const existing = await deps.store.assistants.get(input.assistant_id);
489
+ if (existing) return existing;
490
+ }
491
+ throw error;
492
+ }
493
+ },
494
+ update(assistantId, patch) {
495
+ if (patch.graph_id !== void 0) requireGraph(patch.graph_id);
496
+ return deps.store.assistants.update(assistantId, patch);
497
+ },
498
+ async delete(assistantId, options) {
499
+ await requireAssistant(assistantId);
500
+ if (options?.deleteThreads) {
501
+ const owned = await deletableThreads(assistantId);
502
+ await Promise.all(owned.map((thread) => threads.delete(thread.thread_id)));
503
+ }
504
+ await deps.store.assistants.delete(assistantId);
505
+ },
506
+ async listVersions(assistantId, query) {
507
+ await requireAssistant(assistantId);
508
+ return deps.store.assistants.listVersions(assistantId, query);
509
+ },
510
+ setLatest: (assistantId, version) => deps.store.assistants.setLatest(assistantId, version),
511
+ async drawGraph(assistantId, options) {
512
+ const assistant = await requireAssistant(assistantId);
513
+ const graph = await compileGraph(assistant);
514
+ const drawable = await graph.getGraphAsync({
515
+ ...assistant.config ?? {},
516
+ xray: options?.xray
517
+ });
518
+ return drawable.toJSON();
519
+ },
520
+ async subgraphs(assistantId, options) {
521
+ const assistant = await requireAssistant(assistantId);
522
+ const graph = await compileGraph(assistant);
523
+ const schemas = await deps.graphs.schemas(assistant.graph_id);
524
+ const rootGraphId = Object.keys(schemas).find((key) => !key.includes("|"));
525
+ const result = {};
526
+ for await (const [namespace] of graph.getSubgraphsAsync(
527
+ options?.namespace,
528
+ options?.recurse
529
+ )) {
530
+ const schema = schemas[`${rootGraphId}|${namespace}`] ?? (rootGraphId !== void 0 ? schemas[rootGraphId] : void 0);
531
+ if (schema !== void 0) result[namespace] = schema;
532
+ }
533
+ return result;
312
534
  }
313
535
  };
314
536
  }
@@ -324,11 +546,35 @@ var noopLogger = {
324
546
  error: () => {
325
547
  }
326
548
  };
549
+ var fetchWebhookDispatcher = async (url, payload) => {
550
+ let scheme;
551
+ try {
552
+ scheme = new URL(url).protocol;
553
+ } catch {
554
+ throw new Error(`Webhook URL "${url}" is not a valid absolute URL`);
555
+ }
556
+ if (scheme !== "http:" && scheme !== "https:") {
557
+ throw new Error(`Webhook URL scheme "${scheme}" is not allowed (only http/https)`);
558
+ }
559
+ const send = globalThis.fetch;
560
+ if (!send) {
561
+ throw new Error("global fetch is unavailable; inject a webhookDispatcher to deliver webhooks");
562
+ }
563
+ const response = await send(url, {
564
+ method: "POST",
565
+ headers: { "content-type": "application/json" },
566
+ body: JSON.stringify(payload)
567
+ });
568
+ if (!response.ok) {
569
+ throw new Error(`Webhook POST to ${url} failed with status ${response.status}`);
570
+ }
571
+ };
327
572
  function resolveDeps(deps) {
328
573
  return {
329
574
  ...deps,
330
575
  clock: deps.clock ?? (() => /* @__PURE__ */ new Date()),
331
- logger: deps.logger ?? noopLogger
576
+ logger: deps.logger ?? noopLogger,
577
+ webhookDispatcher: deps.webhookDispatcher ?? fetchWebhookDispatcher
332
578
  };
333
579
  }
334
580
 
@@ -388,7 +634,10 @@ function createContext(deps) {
388
634
  return {
389
635
  deps: resolveDeps(deps),
390
636
  control: new RunControlRegistry(),
391
- locks: new ThreadLocks()
637
+ locks: new ThreadLocks(),
638
+ executionLocks: new ThreadLocks(),
639
+ runBaseCheckpoints: /* @__PURE__ */ new Map(),
640
+ rollbackPlans: /* @__PURE__ */ new Map()
392
641
  };
393
642
  }
394
643
 
@@ -398,6 +647,75 @@ import {
398
647
  SkeinHttpError as SkeinHttpError5
399
648
  } from "@skein-js/core";
400
649
 
650
+ // src/threads/checkpoint-history.ts
651
+ import {
652
+ copyCheckpoint
653
+ } from "@langchain/langgraph";
654
+ async function replayCheckpoints(checkpointer, targetId, tuples) {
655
+ for (const tuple of tuples) {
656
+ const ns = tuple.config.configurable?.checkpoint_ns ?? "";
657
+ const parentId = tuple.parentConfig?.configurable?.checkpoint_id;
658
+ const putConfig = {
659
+ configurable: { thread_id: targetId, checkpoint_ns: ns, checkpoint_id: parentId }
660
+ };
661
+ await checkpointer.put(
662
+ putConfig,
663
+ copyCheckpoint(tuple.checkpoint),
664
+ tuple.metadata ?? {},
665
+ tuple.checkpoint.channel_versions
666
+ );
667
+ if (tuple.pendingWrites && tuple.pendingWrites.length > 0) {
668
+ const writeConfig = {
669
+ configurable: {
670
+ thread_id: targetId,
671
+ checkpoint_ns: ns,
672
+ checkpoint_id: tuple.checkpoint.id
673
+ }
674
+ };
675
+ const byTask = /* @__PURE__ */ new Map();
676
+ for (const [taskId, channel, value] of tuple.pendingWrites) {
677
+ const writes = byTask.get(taskId) ?? [];
678
+ writes.push([channel, value]);
679
+ byTask.set(taskId, writes);
680
+ }
681
+ for (const [taskId, writes] of byTask) {
682
+ await checkpointer.putWrites(writeConfig, writes, taskId);
683
+ }
684
+ }
685
+ }
686
+ }
687
+ async function listCheckpoints(checkpointer, threadId) {
688
+ const tuples = [];
689
+ for await (const tuple of checkpointer.list({ configurable: { thread_id: threadId } })) {
690
+ tuples.push(tuple);
691
+ }
692
+ return tuples;
693
+ }
694
+ async function copyCheckpointHistory(checkpointer, sourceId, targetId) {
695
+ const tuples = await listCheckpoints(checkpointer, sourceId);
696
+ await replayCheckpoints(checkpointer, targetId, tuples.reverse());
697
+ }
698
+ async function rollbackThreadCheckpointsTo(checkpointer, threadId, baseCheckpointId) {
699
+ if (baseCheckpointId === void 0) {
700
+ await checkpointer.deleteThread(threadId);
701
+ return;
702
+ }
703
+ const tuples = await listCheckpoints(checkpointer, threadId);
704
+ const byId = new Map(tuples.map((tuple) => [tuple.checkpoint.id, tuple]));
705
+ if (!byId.has(baseCheckpointId)) return;
706
+ const keep = [];
707
+ let cursor = baseCheckpointId;
708
+ const seen = /* @__PURE__ */ new Set();
709
+ while (cursor !== void 0 && byId.has(cursor) && !seen.has(cursor)) {
710
+ seen.add(cursor);
711
+ const tuple = byId.get(cursor);
712
+ keep.push(tuple);
713
+ cursor = tuple.parentConfig?.configurable?.checkpoint_id;
714
+ }
715
+ await checkpointer.deleteThread(threadId);
716
+ await replayCheckpoints(checkpointer, threadId, keep.reverse());
717
+ }
718
+
401
719
  // src/runs/run-engine.ts
402
720
  import {
403
721
  isTerminalRunStatus as isTerminalRunStatus2,
@@ -405,7 +723,7 @@ import {
405
723
  } from "@skein-js/core";
406
724
 
407
725
  // src/normalize-error.ts
408
- import { isSkeinHttpError, SkeinHttpError as SkeinHttpError3 } from "@skein-js/core";
726
+ import { isSkeinHttpError as isSkeinHttpError2, SkeinHttpError as SkeinHttpError3 } from "@skein-js/core";
409
727
  function serializeError(error) {
410
728
  if (error instanceof Error) {
411
729
  return { name: error.name, message: error.message };
@@ -435,6 +753,18 @@ function chunkToFrameBody(chunk) {
435
753
  function toRunFrame(seq, body) {
436
754
  return { seq, event: body.event, data: body.data };
437
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
+ }
438
768
 
439
769
  // src/store/skein-base-store.ts
440
770
  import {
@@ -601,7 +931,6 @@ function snapshotToThreadState(snapshot) {
601
931
  import { Command } from "@langchain/langgraph";
602
932
  function toGraphMode(mode) {
603
933
  if (mode === "messages-tuple") return "messages";
604
- if (mode === "events") return "updates";
605
934
  return mode;
606
935
  }
607
936
  function normalizeModes(mode) {
@@ -610,6 +939,12 @@ function normalizeModes(mode) {
610
939
  const deduped = [...new Set(mapped)];
611
940
  return deduped.length > 0 ? deduped : ["values"];
612
941
  }
942
+ function wantsEventsMode(mode) {
943
+ return normalizeModes(mode).includes("events");
944
+ }
945
+ function toGraphStreamModes(mode) {
946
+ return normalizeModes(mode).filter((m) => m !== "events");
947
+ }
613
948
  function toGraphInput(kwargs) {
614
949
  if (kwargs.command) {
615
950
  return new Command(kwargs.command);
@@ -622,7 +957,10 @@ var RESERVED_CONFIGURABLE_KEYS = /* @__PURE__ */ new Set([
622
957
  "checkpoint_id",
623
958
  "checkpoint_ns",
624
959
  "checkpoint_map",
625
- "checkpoint"
960
+ "checkpoint",
961
+ "langgraph_auth_user",
962
+ "langgraph_auth_user_id",
963
+ "langgraph_auth_permissions"
626
964
  ]);
627
965
  function sanitizeConfigurable(configurable) {
628
966
  if (!configurable) return {};
@@ -633,11 +971,33 @@ function sanitizeConfigurable(configurable) {
633
971
  }
634
972
  return clean;
635
973
  }
974
+ function withAuthUser(configurable, authUser, scopes) {
975
+ if (!authUser) return configurable;
976
+ return {
977
+ ...configurable,
978
+ langgraph_auth_user: authUser,
979
+ langgraph_auth_user_id: authUser.identity,
980
+ langgraph_auth_permissions: scopes ?? authUser.permissions
981
+ };
982
+ }
983
+ function toFactoryConfigurable(kwargs) {
984
+ const configurable = withAuthUser(
985
+ sanitizeConfigurable(kwargs.config?.configurable),
986
+ kwargs.auth_user,
987
+ kwargs.auth_scopes
988
+ );
989
+ return Object.keys(configurable).length > 0 ? configurable : void 0;
990
+ }
636
991
  function toGraphCallOptions(kwargs, threadId, signal) {
637
992
  const options = {
638
- // Drop server-owned keys from the client's configurable, then force this thread's id.
639
- configurable: { ...sanitizeConfigurable(kwargs.config?.configurable), thread_id: threadId },
640
- streamMode: normalizeModes(kwargs.stream_mode),
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`.
995
+ configurable: withAuthUser(
996
+ { ...sanitizeConfigurable(kwargs.config?.configurable), thread_id: threadId },
997
+ kwargs.auth_user,
998
+ kwargs.auth_scopes
999
+ ),
1000
+ streamMode: toGraphStreamModes(kwargs.stream_mode),
641
1001
  signal
642
1002
  };
643
1003
  if (kwargs.context !== void 0) options.context = kwargs.context;
@@ -676,7 +1036,10 @@ function extractToolActivity(data) {
676
1036
  if (Array.isArray(toolCalls)) {
677
1037
  for (const call of toolCalls) {
678
1038
  if (isRecord(call) && typeof call["name"] === "string" && call["name"].length > 0) {
679
- calls.push({ name: call["name"], id: typeof call["id"] === "string" ? call["id"] : void 0 });
1039
+ calls.push({
1040
+ name: call["name"],
1041
+ id: typeof call["id"] === "string" ? call["id"] : void 0
1042
+ });
680
1043
  }
681
1044
  }
682
1045
  }
@@ -715,9 +1078,19 @@ function describeInterrupts(snapshot) {
715
1078
  }
716
1079
 
717
1080
  // src/runs/run-engine.ts
1081
+ function abortedStatus(reason) {
1082
+ if (reason === "timeout") return "timeout";
1083
+ if (reason === "interrupt") return "interrupted";
1084
+ return "cancelled";
1085
+ }
718
1086
  async function resolveGraph(deps, graphId, kwargs) {
719
1087
  const resolved = await deps.graphs.load(graphId);
720
- const graph = typeof resolved === "function" ? await resolved({ configurable: kwargs.config?.configurable }) : resolved;
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;
721
1094
  graph.checkpointer = deps.checkpointer;
722
1095
  graph.store = new SkeinBaseStore(deps.store.store);
723
1096
  return graph;
@@ -754,6 +1127,9 @@ async function executeRun(deps, exec) {
754
1127
  const runId = run.run_id;
755
1128
  const threadId = run.thread_id;
756
1129
  let seq = 0;
1130
+ let started = false;
1131
+ let outcome = { status: "error", values: {} };
1132
+ let webhookErrorMessage;
757
1133
  const startedAt = deps.clock().getTime();
758
1134
  const loggedTools = /* @__PURE__ */ new Set();
759
1135
  const logToolActivity = (data) => {
@@ -781,10 +1157,23 @@ async function executeRun(deps, exec) {
781
1157
  try {
782
1158
  const current = await deps.store.runs.get(runId);
783
1159
  if (current && isTerminalRunStatus2(current.status)) {
784
- return { status: current.status, values: {} };
1160
+ outcome = { status: current.status, values: {} };
1161
+ return outcome;
785
1162
  }
786
1163
  await deps.store.runs.setStatus(runId, "running");
1164
+ started = true;
787
1165
  await mirrorThreadStatus(deps, threadId, "busy");
1166
+ if (exec.recordBaseCheckpoint) {
1167
+ try {
1168
+ const tip = await deps.checkpointer.getTuple({ configurable: { thread_id: threadId } });
1169
+ exec.recordBaseCheckpoint(tip?.checkpoint.id);
1170
+ } catch (error) {
1171
+ deps.logger.warn(
1172
+ `run ${runId}: failed to read base checkpoint; a rollback of this run won't revert checkpoints`,
1173
+ error
1174
+ );
1175
+ }
1176
+ }
788
1177
  if (deps.logRunActivity) {
789
1178
  deps.logger.info(`run ${runId} started \xB7 assistant=${run.assistant_id} thread=${threadId}`);
790
1179
  }
@@ -795,20 +1184,36 @@ async function executeRun(deps, exec) {
795
1184
  const graph = await resolveGraph(deps, assistant.graph_id, kwargs);
796
1185
  const input = toGraphInput(kwargs);
797
1186
  const options = toGraphCallOptions(kwargs, threadId, control.signal);
798
- const stream = await graph.stream(input, options);
799
- for await (const chunk of stream) {
800
- seq += 1;
801
- const body = chunkToFrameBody(chunk);
802
- await deps.bus.publish(runId, toRunFrame(seq, body));
803
- if (deps.logRunActivity) logToolActivity(body.data);
1187
+ if (wantsEventsMode(kwargs.stream_mode)) {
1188
+ const graphModes = toGraphStreamModes(kwargs.stream_mode);
1189
+ const eventStream = graph.streamEvents(input, {
1190
+ ...options,
1191
+ version: "v2",
1192
+ runId
1193
+ });
1194
+ for await (const event of eventStream) {
1195
+ const body = streamEventToFrameBody(event, runId, graphModes);
1196
+ if (!body) continue;
1197
+ seq += 1;
1198
+ await deps.bus.publish(runId, toRunFrame(seq, body));
1199
+ if (deps.logRunActivity) logToolActivity(body.data);
1200
+ }
1201
+ } else {
1202
+ const stream = await graph.stream(input, options);
1203
+ for await (const chunk of stream) {
1204
+ seq += 1;
1205
+ const body = chunkToFrameBody(chunk);
1206
+ await deps.bus.publish(runId, toRunFrame(seq, body));
1207
+ if (deps.logRunActivity) logToolActivity(body.data);
1208
+ }
804
1209
  }
805
1210
  if (control.signal.aborted) {
806
- const timedOut = control.reason.current === "timeout";
807
- const finalStatus2 = await finalizeRun(deps, runId, timedOut ? "timeout" : "cancelled");
1211
+ const finalStatus2 = await finalizeRun(deps, runId, abortedStatus(control.reason.current));
808
1212
  if (finalStatus2 === "timeout") await mirrorThreadError(deps, threadId, "Run timed out.");
809
- else if (finalStatus2 === "cancelled") await mirrorThreadStatus(deps, threadId, "idle");
1213
+ else await mirrorThreadStatus(deps, threadId, "idle");
810
1214
  logFinished(finalStatus2);
811
- return { status: finalStatus2, values: {} };
1215
+ outcome = { status: finalStatus2, values: {} };
1216
+ return outcome;
812
1217
  }
813
1218
  const snapshot = await graph.getState({ configurable: { thread_id: threadId } });
814
1219
  const computed = runStatusForSnapshot(snapshot);
@@ -825,20 +1230,23 @@ async function executeRun(deps, exec) {
825
1230
  );
826
1231
  }
827
1232
  logFinished(finalStatus);
828
- return { status: finalStatus, values: snapshot.values };
1233
+ outcome = { status: finalStatus, values: snapshot.values };
1234
+ return outcome;
829
1235
  } catch (error) {
830
1236
  const reason = control.reason.current;
831
1237
  if (reason === "timeout") {
832
1238
  const finalStatus2 = await finalizeRun(deps, runId, "timeout");
833
1239
  if (finalStatus2 === "timeout") await mirrorThreadError(deps, threadId, "Run timed out.");
834
1240
  logFinished(finalStatus2);
835
- return { status: finalStatus2, values: {} };
1241
+ outcome = { status: finalStatus2, values: {} };
1242
+ return outcome;
836
1243
  }
837
- if (reason === "cancel" || control.signal.aborted) {
838
- const finalStatus2 = await finalizeRun(deps, runId, "cancelled");
839
- if (finalStatus2 === "cancelled") await mirrorThreadStatus(deps, threadId, "idle");
1244
+ if (reason === "cancel" || reason === "interrupt" || reason === "rollback" || control.signal.aborted) {
1245
+ const finalStatus2 = await finalizeRun(deps, runId, abortedStatus(reason));
1246
+ await mirrorThreadStatus(deps, threadId, "idle");
840
1247
  logFinished(finalStatus2);
841
- return { status: finalStatus2, values: {} };
1248
+ outcome = { status: finalStatus2, values: {} };
1249
+ return outcome;
842
1250
  }
843
1251
  const serialized = serializeError(error);
844
1252
  seq += 1;
@@ -847,15 +1255,76 @@ async function executeRun(deps, exec) {
847
1255
  if (finalStatus === "error") await mirrorThreadError(deps, threadId, serialized.message);
848
1256
  if (deps.logRunActivity) deps.logger.error(`run ${runId} error: ${serialized.message}`);
849
1257
  logFinished(finalStatus);
850
- return { status: finalStatus, values: {} };
1258
+ webhookErrorMessage = serialized.message;
1259
+ outcome = { status: finalStatus, values: {} };
1260
+ return outcome;
851
1261
  } finally {
852
1262
  if (timer !== void 0) clearTimeout(timer);
853
1263
  await deps.bus.close(runId);
1264
+ if (started && kwargs.webhook !== void 0) {
1265
+ const sentAt = deps.clock().toISOString();
1266
+ const payload = {
1267
+ ...run,
1268
+ status: outcome.status,
1269
+ values: outcome.values,
1270
+ run_started_at: new Date(startedAt).toISOString(),
1271
+ run_ended_at: sentAt,
1272
+ webhook_sent_at: sentAt,
1273
+ ...webhookErrorMessage !== void 0 ? { error: webhookErrorMessage } : {}
1274
+ };
1275
+ try {
1276
+ await deps.webhookDispatcher(kwargs.webhook, payload);
1277
+ } catch (error) {
1278
+ deps.logger.warn(`run ${runId}: webhook delivery to ${kwargs.webhook} failed`, error);
1279
+ }
1280
+ }
1281
+ }
1282
+ }
1283
+
1284
+ // src/runs/run-execution.ts
1285
+ async function startRunExecution(ctx, run, kwargs) {
1286
+ const { deps, control, executionLocks, runBaseCheckpoints, rollbackPlans } = ctx;
1287
+ const runId = run.run_id;
1288
+ const threadId = run.thread_id;
1289
+ const runControl = control.register(runId);
1290
+ try {
1291
+ return await executionLocks.run(threadId, async () => {
1292
+ const plan = rollbackPlans.get(runId);
1293
+ if (plan) {
1294
+ rollbackPlans.delete(runId);
1295
+ if (plan.revertToCheckpoint !== false) {
1296
+ try {
1297
+ await rollbackThreadCheckpointsTo(
1298
+ deps.checkpointer,
1299
+ threadId,
1300
+ plan.revertToCheckpoint.baseCheckpointId
1301
+ );
1302
+ } catch (error) {
1303
+ deps.logger.warn(`run ${runId}: rollback of displaced writes failed`, error);
1304
+ }
1305
+ }
1306
+ for (const displacedId of plan.displacedRunIds) {
1307
+ try {
1308
+ await deps.store.runs.delete(displacedId);
1309
+ } catch {
1310
+ }
1311
+ }
1312
+ }
1313
+ return await executeRun(deps, {
1314
+ run,
1315
+ kwargs,
1316
+ control: runControl,
1317
+ recordBaseCheckpoint: (base) => runBaseCheckpoints.set(runId, base)
1318
+ });
1319
+ });
1320
+ } finally {
1321
+ control.clear(runId);
1322
+ runBaseCheckpoints.delete(runId);
854
1323
  }
855
1324
  }
856
1325
 
857
1326
  // src/runs/run-service.ts
858
- function toKwargs(input) {
1327
+ function toKwargs(input, authUser, authScopes) {
859
1328
  const kwargs = {};
860
1329
  if (input.input !== void 0) kwargs.input = input.input;
861
1330
  if (input.command !== void 0) kwargs.command = input.command;
@@ -864,10 +1333,15 @@ function toKwargs(input) {
864
1333
  if (input.stream_mode !== void 0) kwargs.stream_mode = input.stream_mode;
865
1334
  if (input.interrupt_before !== void 0) kwargs.interrupt_before = input.interrupt_before;
866
1335
  if (input.interrupt_after !== void 0) kwargs.interrupt_after = input.interrupt_after;
1336
+ if (input.webhook !== void 0) kwargs.webhook = input.webhook;
1337
+ if (authUser !== void 0) {
1338
+ kwargs.auth_user = authUser;
1339
+ if (authScopes !== void 0) kwargs.auth_scopes = authScopes;
1340
+ }
867
1341
  return kwargs;
868
1342
  }
869
1343
  function createRunService(ctx) {
870
- const { deps, control, locks } = ctx;
1344
+ const { deps, control, locks, runBaseCheckpoints, authUser, authScopes } = ctx;
871
1345
  const requireThread = async (threadId) => {
872
1346
  const thread = await deps.store.threads.get(threadId);
873
1347
  if (!thread) throw SkeinHttpError5.notFound(`Thread "${threadId}" not found.`);
@@ -899,18 +1373,43 @@ function createRunService(ctx) {
899
1373
  const existing = await deps.store.threads.get(threadId);
900
1374
  return existing ?? await deps.store.threads.create({ thread_id: threadId });
901
1375
  };
1376
+ const cancelActiveRun = async (run, reason) => {
1377
+ const terminal = reason === "interrupt" ? "interrupted" : "cancelled";
1378
+ if (run.status === "pending") {
1379
+ await deps.store.runs.setStatus(run.run_id, terminal);
1380
+ await deps.bus.close(run.run_id);
1381
+ control.abort(run.run_id, reason);
1382
+ } else {
1383
+ await deps.store.runs.setStatus(run.run_id, terminal);
1384
+ control.abort(run.run_id, reason);
1385
+ }
1386
+ };
902
1387
  const createPendingRun = async (input, threadId, assistantId, kwargs) => {
903
1388
  return locks.run(threadId, async () => {
904
1389
  const strategy = input.multitask_strategy ?? "reject";
905
- if (await deps.store.runs.hasActiveRun(threadId)) {
906
- if (strategy !== "reject") {
907
- deps.logger.warn(`multitask_strategy "${strategy}" treated as reject (MVP).`);
1390
+ const active = await deps.store.runs.listActiveRuns(threadId);
1391
+ let rollbackPlan;
1392
+ if (active.length > 0) {
1393
+ if (strategy === "reject") {
1394
+ throw SkeinHttpError5.unprocessable(
1395
+ "Thread is already running a task. Wait for it to finish or choose a different multitask strategy.",
1396
+ { code: "thread_busy" }
1397
+ );
1398
+ }
1399
+ if (strategy === "interrupt") {
1400
+ for (const run of active) await cancelActiveRun(run, "interrupt");
1401
+ } else if (strategy === "rollback") {
1402
+ let revertToCheckpoint = false;
1403
+ for (const run of active) {
1404
+ if (runBaseCheckpoints.has(run.run_id)) {
1405
+ revertToCheckpoint = { baseCheckpointId: runBaseCheckpoints.get(run.run_id) };
1406
+ }
1407
+ }
1408
+ rollbackPlan = { revertToCheckpoint, displacedRunIds: active.map((run) => run.run_id) };
1409
+ for (const run of active) await cancelActiveRun(run, "rollback");
908
1410
  }
909
- throw SkeinHttpError5.conflict(`Thread "${threadId}" already has an active run.`, {
910
- code: "thread_busy"
911
- });
912
1411
  }
913
- return deps.store.runs.create({
1412
+ const created = await deps.store.runs.create({
914
1413
  thread_id: threadId,
915
1414
  assistant_id: assistantId,
916
1415
  status: "pending",
@@ -918,20 +1417,16 @@ function createRunService(ctx) {
918
1417
  multitask_strategy: strategy,
919
1418
  kwargs
920
1419
  });
1420
+ if (rollbackPlan) ctx.rollbackPlans.set(created.run_id, rollbackPlan);
1421
+ return created;
921
1422
  });
922
1423
  };
923
- const runInline = (run, kwargs) => {
924
- const runControl = control.register(run.run_id);
925
- const done = executeRun(deps, { run, kwargs, control: runControl }).finally(
926
- () => control.clear(run.run_id)
927
- );
928
- return done;
929
- };
1424
+ const runInline = (run, kwargs) => startRunExecution(ctx, run, kwargs);
930
1425
  return {
931
1426
  async createWait(input) {
932
1427
  const assistant = await requireAssistant(input.assistant_id);
933
1428
  const thread = await ensureThread(input.thread_id);
934
- const kwargs = toKwargs(input);
1429
+ const kwargs = toKwargs(input, authUser, authScopes);
935
1430
  const run = await createPendingRun(input, thread.thread_id, assistant.assistant_id, kwargs);
936
1431
  await stampGraphOnThread(thread, assistant);
937
1432
  const outcome = await runInline(run, kwargs);
@@ -940,7 +1435,7 @@ function createRunService(ctx) {
940
1435
  async createStream(input) {
941
1436
  const assistant = await requireAssistant(input.assistant_id);
942
1437
  const thread = await ensureThread(input.thread_id);
943
- const kwargs = toKwargs(input);
1438
+ const kwargs = toKwargs(input, authUser, authScopes);
944
1439
  const run = await createPendingRun(input, thread.thread_id, assistant.assistant_id, kwargs);
945
1440
  await stampGraphOnThread(thread, assistant);
946
1441
  void runInline(run, kwargs).catch(
@@ -951,7 +1446,7 @@ function createRunService(ctx) {
951
1446
  async createBackground(threadId, input) {
952
1447
  const assistant = await requireAssistant(input.assistant_id);
953
1448
  const thread = await requireThread(threadId);
954
- const kwargs = toKwargs(input);
1449
+ const kwargs = toKwargs(input, authUser, authScopes);
955
1450
  const run = await createPendingRun(
956
1451
  { ...input, thread_id: threadId },
957
1452
  threadId,
@@ -974,6 +1469,7 @@ function createRunService(ctx) {
974
1469
  async cancel(runId) {
975
1470
  const run = await deps.store.runs.get(runId);
976
1471
  if (!run) throw SkeinHttpError5.notFound(`Run "${runId}" not found.`);
1472
+ ctx.rollbackPlans.delete(runId);
977
1473
  if (isTerminalRunStatus3(run.status)) return run;
978
1474
  if (run.status === "pending") {
979
1475
  await deps.store.runs.setStatus(runId, "cancelled");
@@ -992,6 +1488,7 @@ function createRunService(ctx) {
992
1488
  throw SkeinHttpError5.notFound(`Run "${runId}" not found.`);
993
1489
  }
994
1490
  control.abort(runId, "cancel");
1491
+ ctx.rollbackPlans.delete(runId);
995
1492
  await deps.store.runs.delete(runId);
996
1493
  },
997
1494
  async join(runId, afterSeq = 0) {
@@ -1030,50 +1527,10 @@ function createStoreService(deps) {
1030
1527
  }
1031
1528
 
1032
1529
  // src/threads/thread-service.ts
1033
- import {
1034
- copyCheckpoint
1035
- } from "@langchain/langgraph";
1036
1530
  import {
1037
1531
  isTerminalRunStatus as isTerminalRunStatus4,
1038
1532
  SkeinHttpError as SkeinHttpError7
1039
1533
  } from "@skein-js/core";
1040
- async function copyCheckpointHistory(checkpointer, sourceId, targetId) {
1041
- const tuples = [];
1042
- for await (const tuple of checkpointer.list({ configurable: { thread_id: sourceId } })) {
1043
- tuples.push(tuple);
1044
- }
1045
- for (const tuple of tuples.reverse()) {
1046
- const ns = tuple.config.configurable?.checkpoint_ns ?? "";
1047
- const parentId = tuple.parentConfig?.configurable?.checkpoint_id;
1048
- const putConfig = {
1049
- configurable: { thread_id: targetId, checkpoint_ns: ns, checkpoint_id: parentId }
1050
- };
1051
- await checkpointer.put(
1052
- putConfig,
1053
- copyCheckpoint(tuple.checkpoint),
1054
- tuple.metadata ?? {},
1055
- tuple.checkpoint.channel_versions
1056
- );
1057
- if (tuple.pendingWrites && tuple.pendingWrites.length > 0) {
1058
- const writeConfig = {
1059
- configurable: {
1060
- thread_id: targetId,
1061
- checkpoint_ns: ns,
1062
- checkpoint_id: tuple.checkpoint.id
1063
- }
1064
- };
1065
- const byTask = /* @__PURE__ */ new Map();
1066
- for (const [taskId, channel, value] of tuple.pendingWrites) {
1067
- const writes = byTask.get(taskId) ?? [];
1068
- writes.push([channel, value]);
1069
- byTask.set(taskId, writes);
1070
- }
1071
- for (const [taskId, writes] of byTask) {
1072
- await checkpointer.putWrites(writeConfig, writes, taskId);
1073
- }
1074
- }
1075
- }
1076
- }
1077
1534
  function emptyThreadState(threadId) {
1078
1535
  return {
1079
1536
  values: {},
@@ -1237,9 +1694,13 @@ function createThreadStreamService(ctx, runs) {
1237
1694
  // src/service.ts
1238
1695
  function buildProtocolService(ctx) {
1239
1696
  const runs = createRunService(ctx);
1697
+ const threads = createThreadService(ctx);
1240
1698
  return {
1241
- assistants: createAssistantService(ctx.deps),
1242
- threads: createThreadService(ctx),
1699
+ // The assistant service reuses the thread service for its `delete_threads` cascade (abort +
1700
+ // delete), and needs the full context (auth engine + caller) to scope that cascade to the
1701
+ // threads the caller may delete.
1702
+ assistants: createAssistantService(ctx, threads),
1703
+ threads,
1243
1704
  threadStream: createThreadStreamService(ctx, runs),
1244
1705
  runs,
1245
1706
  store: createStoreService(ctx.deps)
@@ -1352,7 +1813,15 @@ var ROUTE_AUTHZ = {
1352
1813
  // assistants
1353
1814
  getAssistant: { resource: "assistants", action: "read" },
1354
1815
  getAssistantSchemas: { resource: "assistants", action: "read" },
1816
+ getAssistantGraph: { resource: "assistants", action: "read" },
1817
+ getAssistantSubgraphs: { resource: "assistants", action: "read" },
1818
+ listAssistantVersions: { resource: "assistants", action: "read" },
1355
1819
  searchAssistants: { resource: "assistants", action: "search" },
1820
+ countAssistants: { resource: "assistants", action: "search" },
1821
+ createAssistant: { resource: "assistants", action: "create" },
1822
+ updateAssistant: { resource: "assistants", action: "update" },
1823
+ setAssistantLatestVersion: { resource: "assistants", action: "update" },
1824
+ deleteAssistant: { resource: "assistants", action: "delete" },
1356
1825
  // threads
1357
1826
  createThread: { resource: "threads", action: "create" },
1358
1827
  copyThread: { resource: "threads", action: "create" },
@@ -1410,7 +1879,7 @@ var STUDIO_USER = {
1410
1879
  };
1411
1880
  function createAuthorizingHandlers(context, engine) {
1412
1881
  const baseHandlers = createProtocolHandlers(buildProtocolService(context));
1413
- const names = Object.keys(baseHandlers);
1882
+ const names = Object.keys(ROUTE_AUTHZ);
1414
1883
  const resolveAuthContext = async (req) => {
1415
1884
  if (!engine.studioAuthDisabled && req.headers["x-auth-scheme"] === "langsmith") {
1416
1885
  return { user: STUDIO_USER, scopes: [] };
@@ -1428,13 +1897,17 @@ function createAuthorizingHandlers(context, engine) {
1428
1897
  value: authValue(req),
1429
1898
  context: authContext
1430
1899
  });
1431
- if (!filters) return baseHandlers[name](req);
1432
- const scopedStore = createAuthScopedStore(context.deps.store, engine, filters, route.resource);
1433
- const scopedContext = {
1900
+ if (!filters && !authContext) return baseHandlers[name](req);
1901
+ const requestContext = {
1434
1902
  ...context,
1435
- deps: { ...context.deps, store: scopedStore }
1903
+ authUser: authContext?.user,
1904
+ authScopes: authContext?.scopes,
1905
+ deps: filters ? {
1906
+ ...context.deps,
1907
+ store: createAuthScopedStore(context.deps.store, engine, filters, route.resource)
1908
+ } : context.deps
1436
1909
  };
1437
- return createProtocolHandlers(buildProtocolService(scopedContext))[name](req);
1910
+ return createProtocolHandlers(buildProtocolService(requestContext))[name](req);
1438
1911
  };
1439
1912
  }
1440
1913
  return wrapped;
@@ -1463,17 +1936,18 @@ function createRunWorker(ctx, options = {}) {
1463
1936
  const inFlight = /* @__PURE__ */ new Set();
1464
1937
  const process = async (queued) => {
1465
1938
  const run = await deps.store.runs.get(queued.run_id);
1466
- if (!run || isTerminalRunStatus6(run.status)) return;
1939
+ if (!run || isTerminalRunStatus6(run.status)) {
1940
+ ctx.rollbackPlans.delete(queued.run_id);
1941
+ return;
1942
+ }
1467
1943
  const kwargs = await deps.store.runs.getKwargs(queued.run_id) ?? {};
1468
- const runControl = control.register(queued.run_id);
1469
1944
  inFlight.add(queued.run_id);
1470
1945
  const startedAt = Date.now();
1471
1946
  let status = "error";
1472
1947
  try {
1473
- status = (await executeRun(deps, { run, kwargs, control: runControl })).status;
1948
+ status = (await startRunExecution(ctx, run, kwargs)).status;
1474
1949
  } finally {
1475
1950
  logRunLifecycle(deps.logger, run, status, startedAt, Date.now());
1476
- control.clear(queued.run_id);
1477
1951
  inFlight.delete(queued.run_id);
1478
1952
  }
1479
1953
  };
@@ -1511,6 +1985,102 @@ function createProtocolRuntime(deps, options = {}) {
1511
1985
  worker: createRunWorker(context, options.worker)
1512
1986
  };
1513
1987
  }
1988
+
1989
+ // src/http/routes.ts
1990
+ var skeinRoutes = [
1991
+ // assistants — literals (search/count) before `:assistant_id`, and nested paths before the bare id.
1992
+ { method: "post", path: "/assistants/search", handler: "searchAssistants" },
1993
+ { method: "post", path: "/assistants/count", handler: "countAssistants" },
1994
+ { method: "post", path: "/assistants", handler: "createAssistant" },
1995
+ { method: "get", path: "/assistants/:assistant_id/schemas", handler: "getAssistantSchemas" },
1996
+ { method: "get", path: "/assistants/:assistant_id/graph", handler: "getAssistantGraph" },
1997
+ {
1998
+ method: "get",
1999
+ path: "/assistants/:assistant_id/subgraphs/:namespace",
2000
+ handler: "getAssistantSubgraphs"
2001
+ },
2002
+ { method: "get", path: "/assistants/:assistant_id/subgraphs", handler: "getAssistantSubgraphs" },
2003
+ { method: "post", path: "/assistants/:assistant_id/versions", handler: "listAssistantVersions" },
2004
+ {
2005
+ method: "post",
2006
+ path: "/assistants/:assistant_id/latest",
2007
+ handler: "setAssistantLatestVersion"
2008
+ },
2009
+ { method: "get", path: "/assistants/:assistant_id", handler: "getAssistant" },
2010
+ { method: "patch", path: "/assistants/:assistant_id", handler: "updateAssistant" },
2011
+ { method: "delete", path: "/assistants/:assistant_id", handler: "deleteAssistant" },
2012
+ // threads
2013
+ { method: "post", path: "/threads/search", handler: "listThreads" },
2014
+ { method: "post", path: "/threads", handler: "createThread" },
2015
+ { method: "post", path: "/threads/:thread_id/copy", handler: "copyThread" },
2016
+ { method: "get", path: "/threads/:thread_id", handler: "getThread" },
2017
+ { method: "patch", path: "/threads/:thread_id", handler: "patchThread" },
2018
+ { method: "delete", path: "/threads/:thread_id", handler: "deleteThread" },
2019
+ { method: "get", path: "/threads/:thread_id/state", handler: "getThreadState" },
2020
+ { method: "post", path: "/threads/:thread_id/history", handler: "getThreadHistory" },
2021
+ // runs — the stateless handlers are reused on the thread-scoped path with the id folded in
2022
+ { method: "post", path: "/runs/wait", handler: "createWaitRun" },
2023
+ { method: "post", path: "/runs/stream", handler: "createStreamRun" },
2024
+ {
2025
+ method: "post",
2026
+ path: "/threads/:thread_id/runs/wait",
2027
+ handler: "createWaitRun",
2028
+ foldThreadIdIntoBody: true
2029
+ },
2030
+ {
2031
+ method: "post",
2032
+ path: "/threads/:thread_id/runs/stream",
2033
+ handler: "createStreamRun",
2034
+ foldThreadIdIntoBody: true
2035
+ },
2036
+ { method: "post", path: "/threads/:thread_id/runs", handler: "createBackgroundRun" },
2037
+ { method: "get", path: "/threads/:thread_id/runs", handler: "listThreadRuns" },
2038
+ { method: "post", path: "/threads/:thread_id/runs/:run_id/cancel", handler: "cancelRun" },
2039
+ { method: "get", path: "/threads/:thread_id/runs/:run_id/stream", handler: "joinRunStream" },
2040
+ { method: "get", path: "/threads/:thread_id/runs/:run_id", handler: "getRun" },
2041
+ { method: "delete", path: "/threads/:thread_id/runs/:run_id", handler: "deleteRun" },
2042
+ { method: "get", path: "/runs/:run_id/stream", handler: "joinRunStream" },
2043
+ // thread streaming / commands
2044
+ { method: "post", path: "/threads/:thread_id/stream", handler: "postThreadStream" },
2045
+ { method: "get", path: "/threads/:thread_id/stream", handler: "getThreadStream" },
2046
+ { method: "post", path: "/threads/:thread_id/commands", handler: "postThreadCommands" },
2047
+ // store
2048
+ { method: "put", path: "/store/items", handler: "putStoreItem" },
2049
+ { method: "get", path: "/store/items", handler: "getStoreItem" },
2050
+ { method: "delete", path: "/store/items", handler: "deleteStoreItem" },
2051
+ { method: "post", path: "/store/items/search", handler: "searchStoreItems" },
2052
+ { method: "post", path: "/store/namespaces", handler: "listStoreNamespaces" }
2053
+ ];
2054
+ function foldThreadId(request) {
2055
+ const threadId = request.params["thread_id"];
2056
+ if (threadId === void 0) return request;
2057
+ const base = typeof request.body === "object" && request.body !== null && !Array.isArray(request.body) ? request.body : {};
2058
+ return { ...request, body: { ...base, thread_id: threadId } };
2059
+ }
2060
+ var compiledRoutes = skeinRoutes.map((binding) => ({
2061
+ binding,
2062
+ regex: new RegExp(`^${binding.path.replace(/:(\w+)/g, "(?<$1>[^/]+)")}$`)
2063
+ }));
2064
+ function matchSkeinRoute(method, pathname) {
2065
+ const wanted = method.toLowerCase();
2066
+ for (const { binding, regex } of compiledRoutes) {
2067
+ if (binding.method !== wanted) continue;
2068
+ const match = regex.exec(pathname);
2069
+ if (match) return { binding, params: decodeParams(match.groups) };
2070
+ }
2071
+ return void 0;
2072
+ }
2073
+ function decodeParams(groups) {
2074
+ const params = {};
2075
+ for (const [key, value] of Object.entries(groups ?? {})) {
2076
+ try {
2077
+ params[key] = decodeURIComponent(value);
2078
+ } catch {
2079
+ params[key] = value;
2080
+ }
2081
+ }
2082
+ return params;
2083
+ }
1514
2084
  export {
1515
2085
  SSE_HEADERS,
1516
2086
  SkeinBaseStore,
@@ -1522,6 +2092,9 @@ export {
1522
2092
  createRunWorker,
1523
2093
  encodeFrame,
1524
2094
  encodeTerminal,
2095
+ foldThreadId,
2096
+ matchSkeinRoute,
1525
2097
  parseAfterSeq,
2098
+ skeinRoutes,
1526
2099
  toSseEvents
1527
2100
  };