@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.
- package/dist/index.d.ts +190 -44
- package/dist/index.js +686 -113
- 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
|
-
|
|
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) =>
|
|
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 {
|
|
284
|
-
|
|
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
|
-
|
|
296
|
-
|
|
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
|
-
|
|
304
|
-
|
|
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,
|
|
639
|
-
|
|
640
|
-
|
|
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({
|
|
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" ?
|
|
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
|
-
|
|
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
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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
|
|
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
|
|
1213
|
+
else await mirrorThreadStatus(deps, threadId, "idle");
|
|
810
1214
|
logFinished(finalStatus2);
|
|
811
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1241
|
+
outcome = { status: finalStatus2, values: {} };
|
|
1242
|
+
return outcome;
|
|
836
1243
|
}
|
|
837
|
-
if (reason === "cancel" || control.signal.aborted) {
|
|
838
|
-
const finalStatus2 = await finalizeRun(deps, runId,
|
|
839
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1242
|
-
|
|
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(
|
|
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
|
|
1433
|
-
const scopedContext = {
|
|
1900
|
+
if (!filters && !authContext) return baseHandlers[name](req);
|
|
1901
|
+
const requestContext = {
|
|
1434
1902
|
...context,
|
|
1435
|
-
|
|
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(
|
|
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))
|
|
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
|
|
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
|
};
|