@skein-js/agent-protocol 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.d.ts +200 -45
- package/dist/index.js +643 -104
- 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);
|
|
@@ -662,7 +997,7 @@ function toGraphCallOptions(kwargs, threadId, signal) {
|
|
|
662
997
|
kwargs.auth_user,
|
|
663
998
|
kwargs.auth_scopes
|
|
664
999
|
),
|
|
665
|
-
streamMode:
|
|
1000
|
+
streamMode: toGraphStreamModes(kwargs.stream_mode),
|
|
666
1001
|
signal
|
|
667
1002
|
};
|
|
668
1003
|
if (kwargs.context !== void 0) options.context = kwargs.context;
|
|
@@ -701,7 +1036,10 @@ function extractToolActivity(data) {
|
|
|
701
1036
|
if (Array.isArray(toolCalls)) {
|
|
702
1037
|
for (const call of toolCalls) {
|
|
703
1038
|
if (isRecord(call) && typeof call["name"] === "string" && call["name"].length > 0) {
|
|
704
|
-
calls.push({
|
|
1039
|
+
calls.push({
|
|
1040
|
+
name: call["name"],
|
|
1041
|
+
id: typeof call["id"] === "string" ? call["id"] : void 0
|
|
1042
|
+
});
|
|
705
1043
|
}
|
|
706
1044
|
}
|
|
707
1045
|
}
|
|
@@ -740,6 +1078,11 @@ function describeInterrupts(snapshot) {
|
|
|
740
1078
|
}
|
|
741
1079
|
|
|
742
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
|
+
}
|
|
743
1086
|
async function resolveGraph(deps, graphId, kwargs) {
|
|
744
1087
|
const resolved = await deps.graphs.load(graphId);
|
|
745
1088
|
const graph = typeof resolved === "function" ? (
|
|
@@ -784,6 +1127,9 @@ async function executeRun(deps, exec) {
|
|
|
784
1127
|
const runId = run.run_id;
|
|
785
1128
|
const threadId = run.thread_id;
|
|
786
1129
|
let seq = 0;
|
|
1130
|
+
let started = false;
|
|
1131
|
+
let outcome = { status: "error", values: {} };
|
|
1132
|
+
let webhookErrorMessage;
|
|
787
1133
|
const startedAt = deps.clock().getTime();
|
|
788
1134
|
const loggedTools = /* @__PURE__ */ new Set();
|
|
789
1135
|
const logToolActivity = (data) => {
|
|
@@ -811,10 +1157,23 @@ async function executeRun(deps, exec) {
|
|
|
811
1157
|
try {
|
|
812
1158
|
const current = await deps.store.runs.get(runId);
|
|
813
1159
|
if (current && isTerminalRunStatus2(current.status)) {
|
|
814
|
-
|
|
1160
|
+
outcome = { status: current.status, values: {} };
|
|
1161
|
+
return outcome;
|
|
815
1162
|
}
|
|
816
1163
|
await deps.store.runs.setStatus(runId, "running");
|
|
1164
|
+
started = true;
|
|
817
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
|
+
}
|
|
818
1177
|
if (deps.logRunActivity) {
|
|
819
1178
|
deps.logger.info(`run ${runId} started \xB7 assistant=${run.assistant_id} thread=${threadId}`);
|
|
820
1179
|
}
|
|
@@ -825,20 +1184,36 @@ async function executeRun(deps, exec) {
|
|
|
825
1184
|
const graph = await resolveGraph(deps, assistant.graph_id, kwargs);
|
|
826
1185
|
const input = toGraphInput(kwargs);
|
|
827
1186
|
const options = toGraphCallOptions(kwargs, threadId, control.signal);
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
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
|
+
}
|
|
834
1209
|
}
|
|
835
1210
|
if (control.signal.aborted) {
|
|
836
|
-
const
|
|
837
|
-
const finalStatus2 = await finalizeRun(deps, runId, timedOut ? "timeout" : "cancelled");
|
|
1211
|
+
const finalStatus2 = await finalizeRun(deps, runId, abortedStatus(control.reason.current));
|
|
838
1212
|
if (finalStatus2 === "timeout") await mirrorThreadError(deps, threadId, "Run timed out.");
|
|
839
|
-
else
|
|
1213
|
+
else await mirrorThreadStatus(deps, threadId, "idle");
|
|
840
1214
|
logFinished(finalStatus2);
|
|
841
|
-
|
|
1215
|
+
outcome = { status: finalStatus2, values: {} };
|
|
1216
|
+
return outcome;
|
|
842
1217
|
}
|
|
843
1218
|
const snapshot = await graph.getState({ configurable: { thread_id: threadId } });
|
|
844
1219
|
const computed = runStatusForSnapshot(snapshot);
|
|
@@ -855,20 +1230,23 @@ async function executeRun(deps, exec) {
|
|
|
855
1230
|
);
|
|
856
1231
|
}
|
|
857
1232
|
logFinished(finalStatus);
|
|
858
|
-
|
|
1233
|
+
outcome = { status: finalStatus, values: snapshot.values };
|
|
1234
|
+
return outcome;
|
|
859
1235
|
} catch (error) {
|
|
860
1236
|
const reason = control.reason.current;
|
|
861
1237
|
if (reason === "timeout") {
|
|
862
1238
|
const finalStatus2 = await finalizeRun(deps, runId, "timeout");
|
|
863
1239
|
if (finalStatus2 === "timeout") await mirrorThreadError(deps, threadId, "Run timed out.");
|
|
864
1240
|
logFinished(finalStatus2);
|
|
865
|
-
|
|
1241
|
+
outcome = { status: finalStatus2, values: {} };
|
|
1242
|
+
return outcome;
|
|
866
1243
|
}
|
|
867
|
-
if (reason === "cancel" || control.signal.aborted) {
|
|
868
|
-
const finalStatus2 = await finalizeRun(deps, runId,
|
|
869
|
-
|
|
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");
|
|
870
1247
|
logFinished(finalStatus2);
|
|
871
|
-
|
|
1248
|
+
outcome = { status: finalStatus2, values: {} };
|
|
1249
|
+
return outcome;
|
|
872
1250
|
}
|
|
873
1251
|
const serialized = serializeError(error);
|
|
874
1252
|
seq += 1;
|
|
@@ -877,10 +1255,71 @@ async function executeRun(deps, exec) {
|
|
|
877
1255
|
if (finalStatus === "error") await mirrorThreadError(deps, threadId, serialized.message);
|
|
878
1256
|
if (deps.logRunActivity) deps.logger.error(`run ${runId} error: ${serialized.message}`);
|
|
879
1257
|
logFinished(finalStatus);
|
|
880
|
-
|
|
1258
|
+
webhookErrorMessage = serialized.message;
|
|
1259
|
+
outcome = { status: finalStatus, values: {} };
|
|
1260
|
+
return outcome;
|
|
881
1261
|
} finally {
|
|
882
1262
|
if (timer !== void 0) clearTimeout(timer);
|
|
883
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);
|
|
884
1323
|
}
|
|
885
1324
|
}
|
|
886
1325
|
|
|
@@ -894,6 +1333,7 @@ function toKwargs(input, authUser, authScopes) {
|
|
|
894
1333
|
if (input.stream_mode !== void 0) kwargs.stream_mode = input.stream_mode;
|
|
895
1334
|
if (input.interrupt_before !== void 0) kwargs.interrupt_before = input.interrupt_before;
|
|
896
1335
|
if (input.interrupt_after !== void 0) kwargs.interrupt_after = input.interrupt_after;
|
|
1336
|
+
if (input.webhook !== void 0) kwargs.webhook = input.webhook;
|
|
897
1337
|
if (authUser !== void 0) {
|
|
898
1338
|
kwargs.auth_user = authUser;
|
|
899
1339
|
if (authScopes !== void 0) kwargs.auth_scopes = authScopes;
|
|
@@ -901,7 +1341,7 @@ function toKwargs(input, authUser, authScopes) {
|
|
|
901
1341
|
return kwargs;
|
|
902
1342
|
}
|
|
903
1343
|
function createRunService(ctx) {
|
|
904
|
-
const { deps, control, locks, authUser, authScopes } = ctx;
|
|
1344
|
+
const { deps, control, locks, runBaseCheckpoints, authUser, authScopes } = ctx;
|
|
905
1345
|
const requireThread = async (threadId) => {
|
|
906
1346
|
const thread = await deps.store.threads.get(threadId);
|
|
907
1347
|
if (!thread) throw SkeinHttpError5.notFound(`Thread "${threadId}" not found.`);
|
|
@@ -933,18 +1373,43 @@ function createRunService(ctx) {
|
|
|
933
1373
|
const existing = await deps.store.threads.get(threadId);
|
|
934
1374
|
return existing ?? await deps.store.threads.create({ thread_id: threadId });
|
|
935
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
|
+
};
|
|
936
1387
|
const createPendingRun = async (input, threadId, assistantId, kwargs) => {
|
|
937
1388
|
return locks.run(threadId, async () => {
|
|
938
1389
|
const strategy = input.multitask_strategy ?? "reject";
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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");
|
|
942
1410
|
}
|
|
943
|
-
throw SkeinHttpError5.conflict(`Thread "${threadId}" already has an active run.`, {
|
|
944
|
-
code: "thread_busy"
|
|
945
|
-
});
|
|
946
1411
|
}
|
|
947
|
-
|
|
1412
|
+
const created = await deps.store.runs.create({
|
|
948
1413
|
thread_id: threadId,
|
|
949
1414
|
assistant_id: assistantId,
|
|
950
1415
|
status: "pending",
|
|
@@ -952,15 +1417,11 @@ function createRunService(ctx) {
|
|
|
952
1417
|
multitask_strategy: strategy,
|
|
953
1418
|
kwargs
|
|
954
1419
|
});
|
|
1420
|
+
if (rollbackPlan) ctx.rollbackPlans.set(created.run_id, rollbackPlan);
|
|
1421
|
+
return created;
|
|
955
1422
|
});
|
|
956
1423
|
};
|
|
957
|
-
const runInline = (run, kwargs) =>
|
|
958
|
-
const runControl = control.register(run.run_id);
|
|
959
|
-
const done = executeRun(deps, { run, kwargs, control: runControl }).finally(
|
|
960
|
-
() => control.clear(run.run_id)
|
|
961
|
-
);
|
|
962
|
-
return done;
|
|
963
|
-
};
|
|
1424
|
+
const runInline = (run, kwargs) => startRunExecution(ctx, run, kwargs);
|
|
964
1425
|
return {
|
|
965
1426
|
async createWait(input) {
|
|
966
1427
|
const assistant = await requireAssistant(input.assistant_id);
|
|
@@ -1008,6 +1469,7 @@ function createRunService(ctx) {
|
|
|
1008
1469
|
async cancel(runId) {
|
|
1009
1470
|
const run = await deps.store.runs.get(runId);
|
|
1010
1471
|
if (!run) throw SkeinHttpError5.notFound(`Run "${runId}" not found.`);
|
|
1472
|
+
ctx.rollbackPlans.delete(runId);
|
|
1011
1473
|
if (isTerminalRunStatus3(run.status)) return run;
|
|
1012
1474
|
if (run.status === "pending") {
|
|
1013
1475
|
await deps.store.runs.setStatus(runId, "cancelled");
|
|
@@ -1026,6 +1488,7 @@ function createRunService(ctx) {
|
|
|
1026
1488
|
throw SkeinHttpError5.notFound(`Run "${runId}" not found.`);
|
|
1027
1489
|
}
|
|
1028
1490
|
control.abort(runId, "cancel");
|
|
1491
|
+
ctx.rollbackPlans.delete(runId);
|
|
1029
1492
|
await deps.store.runs.delete(runId);
|
|
1030
1493
|
},
|
|
1031
1494
|
async join(runId, afterSeq = 0) {
|
|
@@ -1064,50 +1527,10 @@ function createStoreService(deps) {
|
|
|
1064
1527
|
}
|
|
1065
1528
|
|
|
1066
1529
|
// src/threads/thread-service.ts
|
|
1067
|
-
import {
|
|
1068
|
-
copyCheckpoint
|
|
1069
|
-
} from "@langchain/langgraph";
|
|
1070
1530
|
import {
|
|
1071
1531
|
isTerminalRunStatus as isTerminalRunStatus4,
|
|
1072
1532
|
SkeinHttpError as SkeinHttpError7
|
|
1073
1533
|
} from "@skein-js/core";
|
|
1074
|
-
async function copyCheckpointHistory(checkpointer, sourceId, targetId) {
|
|
1075
|
-
const tuples = [];
|
|
1076
|
-
for await (const tuple of checkpointer.list({ configurable: { thread_id: sourceId } })) {
|
|
1077
|
-
tuples.push(tuple);
|
|
1078
|
-
}
|
|
1079
|
-
for (const tuple of tuples.reverse()) {
|
|
1080
|
-
const ns = tuple.config.configurable?.checkpoint_ns ?? "";
|
|
1081
|
-
const parentId = tuple.parentConfig?.configurable?.checkpoint_id;
|
|
1082
|
-
const putConfig = {
|
|
1083
|
-
configurable: { thread_id: targetId, checkpoint_ns: ns, checkpoint_id: parentId }
|
|
1084
|
-
};
|
|
1085
|
-
await checkpointer.put(
|
|
1086
|
-
putConfig,
|
|
1087
|
-
copyCheckpoint(tuple.checkpoint),
|
|
1088
|
-
tuple.metadata ?? {},
|
|
1089
|
-
tuple.checkpoint.channel_versions
|
|
1090
|
-
);
|
|
1091
|
-
if (tuple.pendingWrites && tuple.pendingWrites.length > 0) {
|
|
1092
|
-
const writeConfig = {
|
|
1093
|
-
configurable: {
|
|
1094
|
-
thread_id: targetId,
|
|
1095
|
-
checkpoint_ns: ns,
|
|
1096
|
-
checkpoint_id: tuple.checkpoint.id
|
|
1097
|
-
}
|
|
1098
|
-
};
|
|
1099
|
-
const byTask = /* @__PURE__ */ new Map();
|
|
1100
|
-
for (const [taskId, channel, value] of tuple.pendingWrites) {
|
|
1101
|
-
const writes = byTask.get(taskId) ?? [];
|
|
1102
|
-
writes.push([channel, value]);
|
|
1103
|
-
byTask.set(taskId, writes);
|
|
1104
|
-
}
|
|
1105
|
-
for (const [taskId, writes] of byTask) {
|
|
1106
|
-
await checkpointer.putWrites(writeConfig, writes, taskId);
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
1534
|
function emptyThreadState(threadId) {
|
|
1112
1535
|
return {
|
|
1113
1536
|
values: {},
|
|
@@ -1269,19 +1692,24 @@ function createThreadStreamService(ctx, runs) {
|
|
|
1269
1692
|
}
|
|
1270
1693
|
|
|
1271
1694
|
// src/service.ts
|
|
1272
|
-
function
|
|
1695
|
+
function createProtocolServiceFromContext(ctx) {
|
|
1273
1696
|
const runs = createRunService(ctx);
|
|
1697
|
+
const threads = createThreadService(ctx);
|
|
1274
1698
|
return {
|
|
1275
|
-
|
|
1276
|
-
|
|
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,
|
|
1277
1704
|
threadStream: createThreadStreamService(ctx, runs),
|
|
1278
1705
|
runs,
|
|
1279
1706
|
store: createStoreService(ctx.deps)
|
|
1280
1707
|
};
|
|
1281
1708
|
}
|
|
1282
1709
|
function createProtocolService(deps) {
|
|
1283
|
-
return
|
|
1710
|
+
return createProtocolServiceFromContext(createContext(deps));
|
|
1284
1711
|
}
|
|
1712
|
+
var buildProtocolService = createProtocolServiceFromContext;
|
|
1285
1713
|
|
|
1286
1714
|
// src/auth/auth-scoped-store.ts
|
|
1287
1715
|
import {
|
|
@@ -1386,7 +1814,15 @@ var ROUTE_AUTHZ = {
|
|
|
1386
1814
|
// assistants
|
|
1387
1815
|
getAssistant: { resource: "assistants", action: "read" },
|
|
1388
1816
|
getAssistantSchemas: { resource: "assistants", action: "read" },
|
|
1817
|
+
getAssistantGraph: { resource: "assistants", action: "read" },
|
|
1818
|
+
getAssistantSubgraphs: { resource: "assistants", action: "read" },
|
|
1819
|
+
listAssistantVersions: { resource: "assistants", action: "read" },
|
|
1389
1820
|
searchAssistants: { resource: "assistants", action: "search" },
|
|
1821
|
+
countAssistants: { resource: "assistants", action: "search" },
|
|
1822
|
+
createAssistant: { resource: "assistants", action: "create" },
|
|
1823
|
+
updateAssistant: { resource: "assistants", action: "update" },
|
|
1824
|
+
setAssistantLatestVersion: { resource: "assistants", action: "update" },
|
|
1825
|
+
deleteAssistant: { resource: "assistants", action: "delete" },
|
|
1390
1826
|
// threads
|
|
1391
1827
|
createThread: { resource: "threads", action: "create" },
|
|
1392
1828
|
copyThread: { resource: "threads", action: "create" },
|
|
@@ -1443,7 +1879,7 @@ var STUDIO_USER = {
|
|
|
1443
1879
|
permissions: []
|
|
1444
1880
|
};
|
|
1445
1881
|
function createAuthorizingHandlers(context, engine) {
|
|
1446
|
-
const baseHandlers = createProtocolHandlers(
|
|
1882
|
+
const baseHandlers = createProtocolHandlers(createProtocolServiceFromContext(context));
|
|
1447
1883
|
const names = Object.keys(ROUTE_AUTHZ);
|
|
1448
1884
|
const resolveAuthContext = async (req) => {
|
|
1449
1885
|
if (!engine.studioAuthDisabled && req.headers["x-auth-scheme"] === "langsmith") {
|
|
@@ -1472,7 +1908,7 @@ function createAuthorizingHandlers(context, engine) {
|
|
|
1472
1908
|
store: createAuthScopedStore(context.deps.store, engine, filters, route.resource)
|
|
1473
1909
|
} : context.deps
|
|
1474
1910
|
};
|
|
1475
|
-
return createProtocolHandlers(
|
|
1911
|
+
return createProtocolHandlers(createProtocolServiceFromContext(requestContext))[name](req);
|
|
1476
1912
|
};
|
|
1477
1913
|
}
|
|
1478
1914
|
return wrapped;
|
|
@@ -1501,17 +1937,18 @@ function createRunWorker(ctx, options = {}) {
|
|
|
1501
1937
|
const inFlight = /* @__PURE__ */ new Set();
|
|
1502
1938
|
const process = async (queued) => {
|
|
1503
1939
|
const run = await deps.store.runs.get(queued.run_id);
|
|
1504
|
-
if (!run || isTerminalRunStatus6(run.status))
|
|
1940
|
+
if (!run || isTerminalRunStatus6(run.status)) {
|
|
1941
|
+
ctx.rollbackPlans.delete(queued.run_id);
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1505
1944
|
const kwargs = await deps.store.runs.getKwargs(queued.run_id) ?? {};
|
|
1506
|
-
const runControl = control.register(queued.run_id);
|
|
1507
1945
|
inFlight.add(queued.run_id);
|
|
1508
1946
|
const startedAt = Date.now();
|
|
1509
1947
|
let status = "error";
|
|
1510
1948
|
try {
|
|
1511
|
-
status = (await
|
|
1949
|
+
status = (await startRunExecution(ctx, run, kwargs)).status;
|
|
1512
1950
|
} finally {
|
|
1513
1951
|
logRunLifecycle(deps.logger, run, status, startedAt, Date.now());
|
|
1514
|
-
control.clear(queued.run_id);
|
|
1515
1952
|
inFlight.delete(queued.run_id);
|
|
1516
1953
|
}
|
|
1517
1954
|
};
|
|
@@ -1541,7 +1978,7 @@ function createRunWorker(ctx, options = {}) {
|
|
|
1541
1978
|
// src/runtime.ts
|
|
1542
1979
|
function createProtocolRuntime(deps, options = {}) {
|
|
1543
1980
|
const context = createContext(deps);
|
|
1544
|
-
const service =
|
|
1981
|
+
const service = createProtocolServiceFromContext(context);
|
|
1545
1982
|
const handlers = deps.auth ? createAuthorizingHandlers(context, deps.auth) : createProtocolHandlers(service);
|
|
1546
1983
|
return {
|
|
1547
1984
|
service,
|
|
@@ -1549,17 +1986,119 @@ function createProtocolRuntime(deps, options = {}) {
|
|
|
1549
1986
|
worker: createRunWorker(context, options.worker)
|
|
1550
1987
|
};
|
|
1551
1988
|
}
|
|
1989
|
+
|
|
1990
|
+
// src/http/routes.ts
|
|
1991
|
+
var skeinRoutes = [
|
|
1992
|
+
// assistants — literals (search/count) before `:assistant_id`, and nested paths before the bare id.
|
|
1993
|
+
{ method: "post", path: "/assistants/search", handler: "searchAssistants" },
|
|
1994
|
+
{ method: "post", path: "/assistants/count", handler: "countAssistants" },
|
|
1995
|
+
{ method: "post", path: "/assistants", handler: "createAssistant" },
|
|
1996
|
+
{ method: "get", path: "/assistants/:assistant_id/schemas", handler: "getAssistantSchemas" },
|
|
1997
|
+
{ method: "get", path: "/assistants/:assistant_id/graph", handler: "getAssistantGraph" },
|
|
1998
|
+
{
|
|
1999
|
+
method: "get",
|
|
2000
|
+
path: "/assistants/:assistant_id/subgraphs/:namespace",
|
|
2001
|
+
handler: "getAssistantSubgraphs"
|
|
2002
|
+
},
|
|
2003
|
+
{ method: "get", path: "/assistants/:assistant_id/subgraphs", handler: "getAssistantSubgraphs" },
|
|
2004
|
+
{ method: "post", path: "/assistants/:assistant_id/versions", handler: "listAssistantVersions" },
|
|
2005
|
+
{
|
|
2006
|
+
method: "post",
|
|
2007
|
+
path: "/assistants/:assistant_id/latest",
|
|
2008
|
+
handler: "setAssistantLatestVersion"
|
|
2009
|
+
},
|
|
2010
|
+
{ method: "get", path: "/assistants/:assistant_id", handler: "getAssistant" },
|
|
2011
|
+
{ method: "patch", path: "/assistants/:assistant_id", handler: "updateAssistant" },
|
|
2012
|
+
{ method: "delete", path: "/assistants/:assistant_id", handler: "deleteAssistant" },
|
|
2013
|
+
// threads
|
|
2014
|
+
{ method: "post", path: "/threads/search", handler: "listThreads" },
|
|
2015
|
+
{ method: "post", path: "/threads", handler: "createThread" },
|
|
2016
|
+
{ method: "post", path: "/threads/:thread_id/copy", handler: "copyThread" },
|
|
2017
|
+
{ method: "get", path: "/threads/:thread_id", handler: "getThread" },
|
|
2018
|
+
{ method: "patch", path: "/threads/:thread_id", handler: "patchThread" },
|
|
2019
|
+
{ method: "delete", path: "/threads/:thread_id", handler: "deleteThread" },
|
|
2020
|
+
{ method: "get", path: "/threads/:thread_id/state", handler: "getThreadState" },
|
|
2021
|
+
{ method: "post", path: "/threads/:thread_id/history", handler: "getThreadHistory" },
|
|
2022
|
+
// runs — the stateless handlers are reused on the thread-scoped path with the id folded in
|
|
2023
|
+
{ method: "post", path: "/runs/wait", handler: "createWaitRun" },
|
|
2024
|
+
{ method: "post", path: "/runs/stream", handler: "createStreamRun" },
|
|
2025
|
+
{
|
|
2026
|
+
method: "post",
|
|
2027
|
+
path: "/threads/:thread_id/runs/wait",
|
|
2028
|
+
handler: "createWaitRun",
|
|
2029
|
+
foldThreadIdIntoBody: true
|
|
2030
|
+
},
|
|
2031
|
+
{
|
|
2032
|
+
method: "post",
|
|
2033
|
+
path: "/threads/:thread_id/runs/stream",
|
|
2034
|
+
handler: "createStreamRun",
|
|
2035
|
+
foldThreadIdIntoBody: true
|
|
2036
|
+
},
|
|
2037
|
+
{ method: "post", path: "/threads/:thread_id/runs", handler: "createBackgroundRun" },
|
|
2038
|
+
{ method: "get", path: "/threads/:thread_id/runs", handler: "listThreadRuns" },
|
|
2039
|
+
{ method: "post", path: "/threads/:thread_id/runs/:run_id/cancel", handler: "cancelRun" },
|
|
2040
|
+
{ method: "get", path: "/threads/:thread_id/runs/:run_id/stream", handler: "joinRunStream" },
|
|
2041
|
+
{ method: "get", path: "/threads/:thread_id/runs/:run_id", handler: "getRun" },
|
|
2042
|
+
{ method: "delete", path: "/threads/:thread_id/runs/:run_id", handler: "deleteRun" },
|
|
2043
|
+
{ method: "get", path: "/runs/:run_id/stream", handler: "joinRunStream" },
|
|
2044
|
+
// thread streaming / commands
|
|
2045
|
+
{ method: "post", path: "/threads/:thread_id/stream", handler: "postThreadStream" },
|
|
2046
|
+
{ method: "get", path: "/threads/:thread_id/stream", handler: "getThreadStream" },
|
|
2047
|
+
{ method: "post", path: "/threads/:thread_id/commands", handler: "postThreadCommands" },
|
|
2048
|
+
// store
|
|
2049
|
+
{ method: "put", path: "/store/items", handler: "putStoreItem" },
|
|
2050
|
+
{ method: "get", path: "/store/items", handler: "getStoreItem" },
|
|
2051
|
+
{ method: "delete", path: "/store/items", handler: "deleteStoreItem" },
|
|
2052
|
+
{ method: "post", path: "/store/items/search", handler: "searchStoreItems" },
|
|
2053
|
+
{ method: "post", path: "/store/namespaces", handler: "listStoreNamespaces" }
|
|
2054
|
+
];
|
|
2055
|
+
function copyThreadIdIntoBody(request) {
|
|
2056
|
+
const threadId = request.params["thread_id"];
|
|
2057
|
+
if (threadId === void 0) return request;
|
|
2058
|
+
const base = typeof request.body === "object" && request.body !== null && !Array.isArray(request.body) ? request.body : {};
|
|
2059
|
+
return { ...request, body: { ...base, thread_id: threadId } };
|
|
2060
|
+
}
|
|
2061
|
+
var foldThreadId = copyThreadIdIntoBody;
|
|
2062
|
+
var compiledRoutes = skeinRoutes.map((binding) => ({
|
|
2063
|
+
binding,
|
|
2064
|
+
regex: new RegExp(`^${binding.path.replace(/:(\w+)/g, "(?<$1>[^/]+)")}$`)
|
|
2065
|
+
}));
|
|
2066
|
+
function matchSkeinRoute(method, pathname) {
|
|
2067
|
+
const wanted = method.toLowerCase();
|
|
2068
|
+
for (const { binding, regex } of compiledRoutes) {
|
|
2069
|
+
if (binding.method !== wanted) continue;
|
|
2070
|
+
const match = regex.exec(pathname);
|
|
2071
|
+
if (match) return { binding, params: decodeParams(match.groups) };
|
|
2072
|
+
}
|
|
2073
|
+
return void 0;
|
|
2074
|
+
}
|
|
2075
|
+
function decodeParams(groups) {
|
|
2076
|
+
const params = {};
|
|
2077
|
+
for (const [key, value] of Object.entries(groups ?? {})) {
|
|
2078
|
+
try {
|
|
2079
|
+
params[key] = decodeURIComponent(value);
|
|
2080
|
+
} catch {
|
|
2081
|
+
params[key] = value;
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
return params;
|
|
2085
|
+
}
|
|
1552
2086
|
export {
|
|
1553
2087
|
SSE_HEADERS,
|
|
1554
2088
|
SkeinBaseStore,
|
|
1555
2089
|
buildProtocolService,
|
|
2090
|
+
copyThreadIdIntoBody,
|
|
1556
2091
|
createContext,
|
|
1557
2092
|
createProtocolHandlers,
|
|
1558
2093
|
createProtocolRuntime,
|
|
1559
2094
|
createProtocolService,
|
|
2095
|
+
createProtocolServiceFromContext,
|
|
1560
2096
|
createRunWorker,
|
|
1561
2097
|
encodeFrame,
|
|
1562
2098
|
encodeTerminal,
|
|
2099
|
+
foldThreadId,
|
|
2100
|
+
matchSkeinRoute,
|
|
1563
2101
|
parseAfterSeq,
|
|
2102
|
+
skeinRoutes,
|
|
1564
2103
|
toSseEvents
|
|
1565
2104
|
};
|