@skein-js/agent-protocol 0.1.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/LICENSE +202 -0
- package/README.md +72 -0
- package/dist/index.d.ts +370 -0
- package/dist/index.js +1382 -0
- package/package.json +58 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1382 @@
|
|
|
1
|
+
// src/sse/sse.ts
|
|
2
|
+
import { serializeWireJson } from "@skein-js/core";
|
|
3
|
+
var SSE_HEADERS = {
|
|
4
|
+
"content-type": "text/event-stream",
|
|
5
|
+
"cache-control": "no-cache, no-transform",
|
|
6
|
+
connection: "keep-alive"
|
|
7
|
+
};
|
|
8
|
+
function encodeFrame(frame) {
|
|
9
|
+
return `id: ${frame.seq}
|
|
10
|
+
event: ${frame.event}
|
|
11
|
+
data: ${serializeWireJson(frame.data)}
|
|
12
|
+
|
|
13
|
+
`;
|
|
14
|
+
}
|
|
15
|
+
function encodeTerminal(status) {
|
|
16
|
+
const event = status === "error" || status === "timeout" ? "error" : "end";
|
|
17
|
+
return `event: ${event}
|
|
18
|
+
data: ${JSON.stringify({ status })}
|
|
19
|
+
|
|
20
|
+
`;
|
|
21
|
+
}
|
|
22
|
+
async function* toSseEvents(frames, finalStatus) {
|
|
23
|
+
let sawErrorFrame = false;
|
|
24
|
+
for await (const frame of frames) {
|
|
25
|
+
if (frame.event === "error") sawErrorFrame = true;
|
|
26
|
+
yield encodeFrame(frame);
|
|
27
|
+
}
|
|
28
|
+
const status = await finalStatus();
|
|
29
|
+
const terminal = status ?? "success";
|
|
30
|
+
if ((terminal === "error" || terminal === "timeout") && sawErrorFrame) return;
|
|
31
|
+
yield encodeTerminal(terminal);
|
|
32
|
+
}
|
|
33
|
+
function parseAfterSeq(lastEventId) {
|
|
34
|
+
if (lastEventId === void 0) return 0;
|
|
35
|
+
const parsed = Number.parseInt(lastEventId, 10);
|
|
36
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/validation/parse.ts
|
|
40
|
+
import { SkeinHttpError } from "@skein-js/core";
|
|
41
|
+
function parse(schema, raw, subject = "request body") {
|
|
42
|
+
const result = schema.safeParse(raw);
|
|
43
|
+
if (!result.success) {
|
|
44
|
+
throw SkeinHttpError.badRequest(`Invalid ${subject}.`, { details: result.error.flatten() });
|
|
45
|
+
}
|
|
46
|
+
return result.data;
|
|
47
|
+
}
|
|
48
|
+
function requireParam(params, name) {
|
|
49
|
+
const value = params[name];
|
|
50
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
51
|
+
throw SkeinHttpError.badRequest(`Missing path parameter "${name}".`);
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/validation/schemas.ts
|
|
57
|
+
import { z } from "zod";
|
|
58
|
+
var commandSchema = z.object({
|
|
59
|
+
resume: z.unknown().optional(),
|
|
60
|
+
update: z.unknown().optional(),
|
|
61
|
+
goto: z.unknown().optional()
|
|
62
|
+
}).passthrough();
|
|
63
|
+
var streamModeSchema = z.union([z.string(), z.array(z.string())]);
|
|
64
|
+
var configSchema = z.record(z.unknown());
|
|
65
|
+
var multitaskStrategySchema = z.enum(["reject", "interrupt", "rollback", "enqueue"]);
|
|
66
|
+
var interruptWhenSchema = z.union([z.array(z.string()), z.literal("*")]);
|
|
67
|
+
var runCreateSchema = z.object({
|
|
68
|
+
assistant_id: z.string().min(1),
|
|
69
|
+
thread_id: z.string().min(1).optional(),
|
|
70
|
+
input: z.unknown().optional(),
|
|
71
|
+
command: commandSchema.optional(),
|
|
72
|
+
config: configSchema.optional(),
|
|
73
|
+
context: z.unknown().optional(),
|
|
74
|
+
stream_mode: streamModeSchema.optional(),
|
|
75
|
+
metadata: z.record(z.unknown()).optional(),
|
|
76
|
+
multitask_strategy: multitaskStrategySchema.optional(),
|
|
77
|
+
interrupt_before: interruptWhenSchema.optional(),
|
|
78
|
+
interrupt_after: interruptWhenSchema.optional()
|
|
79
|
+
}).passthrough();
|
|
80
|
+
var threadStreamSchema = z.object({
|
|
81
|
+
assistant_id: z.string().min(1),
|
|
82
|
+
input: z.unknown().optional(),
|
|
83
|
+
config: configSchema.optional(),
|
|
84
|
+
context: z.unknown().optional(),
|
|
85
|
+
stream_mode: streamModeSchema.optional(),
|
|
86
|
+
metadata: z.record(z.unknown()).optional()
|
|
87
|
+
}).passthrough();
|
|
88
|
+
var commandBodySchema = z.object({
|
|
89
|
+
assistant_id: z.string().min(1).optional(),
|
|
90
|
+
command: commandSchema.optional(),
|
|
91
|
+
resume: z.unknown().optional(),
|
|
92
|
+
stream_mode: streamModeSchema.optional(),
|
|
93
|
+
config: configSchema.optional(),
|
|
94
|
+
context: z.unknown().optional(),
|
|
95
|
+
metadata: z.record(z.unknown()).optional()
|
|
96
|
+
}).passthrough();
|
|
97
|
+
var threadCreateSchema = z.object({
|
|
98
|
+
thread_id: z.string().min(1).optional(),
|
|
99
|
+
metadata: z.record(z.unknown()).optional()
|
|
100
|
+
}).passthrough();
|
|
101
|
+
var threadPatchSchema = z.object({
|
|
102
|
+
metadata: z.record(z.unknown()).optional()
|
|
103
|
+
}).passthrough();
|
|
104
|
+
var assistantSearchSchema = z.object({
|
|
105
|
+
graph_id: z.string().optional(),
|
|
106
|
+
limit: z.number().int().positive().optional(),
|
|
107
|
+
offset: z.number().int().nonnegative().optional()
|
|
108
|
+
}).passthrough();
|
|
109
|
+
var storePutSchema = z.object({
|
|
110
|
+
namespace: z.array(z.string()).min(1),
|
|
111
|
+
key: z.string().min(1),
|
|
112
|
+
value: z.record(z.unknown())
|
|
113
|
+
}).passthrough();
|
|
114
|
+
var storeSearchSchema = z.object({
|
|
115
|
+
namespace_prefix: z.array(z.string()).optional(),
|
|
116
|
+
query: z.string().optional(),
|
|
117
|
+
limit: z.number().int().positive().optional(),
|
|
118
|
+
offset: z.number().int().nonnegative().optional()
|
|
119
|
+
}).passthrough();
|
|
120
|
+
var listNamespacesSchema = z.object({
|
|
121
|
+
prefix: z.array(z.string()).optional()
|
|
122
|
+
}).passthrough();
|
|
123
|
+
|
|
124
|
+
// src/create-handlers.ts
|
|
125
|
+
var json = (body, status = 200) => ({ kind: "json", status, body });
|
|
126
|
+
var empty = (status = 204) => ({ kind: "empty", status });
|
|
127
|
+
function queryValue(value) {
|
|
128
|
+
return Array.isArray(value) ? value[0] : value;
|
|
129
|
+
}
|
|
130
|
+
function namespaceFromQuery(value) {
|
|
131
|
+
if (Array.isArray(value)) return value;
|
|
132
|
+
if (typeof value === "string" && value.length > 0) return value.split(".");
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
function positiveIntQuery(value) {
|
|
136
|
+
const raw = queryValue(value);
|
|
137
|
+
if (raw === void 0) return void 0;
|
|
138
|
+
const parsed = Number.parseInt(raw, 10);
|
|
139
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : void 0;
|
|
140
|
+
}
|
|
141
|
+
function createProtocolHandlers(service) {
|
|
142
|
+
const sse = (runId, frames) => ({
|
|
143
|
+
kind: "sse",
|
|
144
|
+
status: 200,
|
|
145
|
+
events: toSseEvents(frames, () => service.runs.finalStatus(runId))
|
|
146
|
+
});
|
|
147
|
+
const afterSeqFrom = (req) => parseAfterSeq(req.headers["last-event-id"]);
|
|
148
|
+
return {
|
|
149
|
+
// --- assistants ---------------------------------------------------------------------------
|
|
150
|
+
getAssistant: async (req) => json(await service.assistants.get(requireParam(req.params, "assistant_id"))),
|
|
151
|
+
searchAssistants: async (req) => json(await service.assistants.search(parse(assistantSearchSchema, req.body ?? {}))),
|
|
152
|
+
getAssistantSchemas: async (req) => json(await service.assistants.schemas(requireParam(req.params, "assistant_id"))),
|
|
153
|
+
// --- threads ------------------------------------------------------------------------------
|
|
154
|
+
createThread: async (req) => json(await service.threads.create(parse(threadCreateSchema, req.body ?? {}))),
|
|
155
|
+
getThread: async (req) => json(await service.threads.get(requireParam(req.params, "thread_id"))),
|
|
156
|
+
listThreads: async () => json(await service.threads.list()),
|
|
157
|
+
patchThread: async (req) => json(
|
|
158
|
+
await service.threads.patch(
|
|
159
|
+
requireParam(req.params, "thread_id"),
|
|
160
|
+
parse(threadPatchSchema, req.body ?? {})
|
|
161
|
+
)
|
|
162
|
+
),
|
|
163
|
+
deleteThread: async (req) => {
|
|
164
|
+
await service.threads.delete(requireParam(req.params, "thread_id"));
|
|
165
|
+
return empty();
|
|
166
|
+
},
|
|
167
|
+
getThreadHistory: async (req) => {
|
|
168
|
+
const limit = positiveIntQuery(req.query["limit"]);
|
|
169
|
+
const options = limit === void 0 ? void 0 : { limit };
|
|
170
|
+
return json(await service.threads.history(requireParam(req.params, "thread_id"), options));
|
|
171
|
+
},
|
|
172
|
+
getThreadState: async (req) => json(await service.threads.getState(requireParam(req.params, "thread_id"))),
|
|
173
|
+
// --- runs ---------------------------------------------------------------------------------
|
|
174
|
+
createWaitRun: async (req) => json(await service.runs.createWait(parse(runCreateSchema, req.body))),
|
|
175
|
+
createStreamRun: async (req) => {
|
|
176
|
+
const started = await service.runs.createStream(
|
|
177
|
+
parse(runCreateSchema, req.body)
|
|
178
|
+
);
|
|
179
|
+
return sse(started.runId, started.frames);
|
|
180
|
+
},
|
|
181
|
+
createBackgroundRun: async (req) => json(
|
|
182
|
+
await service.runs.createBackground(
|
|
183
|
+
requireParam(req.params, "thread_id"),
|
|
184
|
+
parse(runCreateSchema, req.body)
|
|
185
|
+
)
|
|
186
|
+
),
|
|
187
|
+
getRun: async (req) => json(await service.runs.get(requireParam(req.params, "run_id"))),
|
|
188
|
+
listThreadRuns: async (req) => json(await service.runs.listByThread(requireParam(req.params, "thread_id"))),
|
|
189
|
+
joinRunStream: async (req) => {
|
|
190
|
+
const runId = requireParam(req.params, "run_id");
|
|
191
|
+
const frames = await service.runs.join(runId, afterSeqFrom(req));
|
|
192
|
+
return sse(runId, frames);
|
|
193
|
+
},
|
|
194
|
+
cancelRun: async (req) => json(await service.runs.cancel(requireParam(req.params, "run_id"))),
|
|
195
|
+
deleteRun: async (req) => {
|
|
196
|
+
await service.runs.delete(requireParam(req.params, "run_id"));
|
|
197
|
+
return empty();
|
|
198
|
+
},
|
|
199
|
+
// --- thread streaming / commands ----------------------------------------------------------
|
|
200
|
+
postThreadStream: async (req) => {
|
|
201
|
+
const started = await service.threadStream.stream(
|
|
202
|
+
requireParam(req.params, "thread_id"),
|
|
203
|
+
parse(threadStreamSchema, req.body)
|
|
204
|
+
);
|
|
205
|
+
return sse(started.runId, started.frames);
|
|
206
|
+
},
|
|
207
|
+
getThreadStream: async (req) => {
|
|
208
|
+
const started = await service.threadStream.joinStream(
|
|
209
|
+
requireParam(req.params, "thread_id"),
|
|
210
|
+
afterSeqFrom(req)
|
|
211
|
+
);
|
|
212
|
+
return sse(started.runId, started.frames);
|
|
213
|
+
},
|
|
214
|
+
postThreadCommands: async (req) => {
|
|
215
|
+
const started = await service.threadStream.command(
|
|
216
|
+
requireParam(req.params, "thread_id"),
|
|
217
|
+
parse(commandBodySchema, req.body ?? {})
|
|
218
|
+
);
|
|
219
|
+
return sse(started.runId, started.frames);
|
|
220
|
+
},
|
|
221
|
+
// --- store --------------------------------------------------------------------------------
|
|
222
|
+
putStoreItem: async (req) => {
|
|
223
|
+
const body = parse(storePutSchema, req.body);
|
|
224
|
+
return json(await service.store.put(body.namespace, body.key, body.value));
|
|
225
|
+
},
|
|
226
|
+
getStoreItem: async (req) => {
|
|
227
|
+
const namespace = namespaceFromQuery(req.query["namespace"]);
|
|
228
|
+
const key = queryValue(req.query["key"]) ?? "";
|
|
229
|
+
return json(await service.store.get(namespace, key));
|
|
230
|
+
},
|
|
231
|
+
deleteStoreItem: async (req) => {
|
|
232
|
+
const namespace = namespaceFromQuery(req.query["namespace"]);
|
|
233
|
+
const key = queryValue(req.query["key"]) ?? "";
|
|
234
|
+
await service.store.delete(namespace, key);
|
|
235
|
+
return empty();
|
|
236
|
+
},
|
|
237
|
+
searchStoreItems: async (req) => {
|
|
238
|
+
const body = parse(storeSearchSchema, req.body ?? {});
|
|
239
|
+
return json(
|
|
240
|
+
await service.store.search({
|
|
241
|
+
prefix: body.namespace_prefix,
|
|
242
|
+
query: body.query,
|
|
243
|
+
limit: body.limit,
|
|
244
|
+
offset: body.offset
|
|
245
|
+
})
|
|
246
|
+
);
|
|
247
|
+
},
|
|
248
|
+
listStoreNamespaces: async (req) => {
|
|
249
|
+
const body = parse(listNamespacesSchema, req.body ?? {});
|
|
250
|
+
return json(await service.store.listNamespaces(body.prefix));
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/assistants/assistant-service.ts
|
|
256
|
+
import { SkeinHttpError as SkeinHttpError2 } from "@skein-js/core";
|
|
257
|
+
function createAssistantService(deps) {
|
|
258
|
+
const requireAssistant = async (assistantId) => {
|
|
259
|
+
const assistant = await deps.store.assistants.get(assistantId);
|
|
260
|
+
if (!assistant) throw SkeinHttpError2.notFound(`Assistant "${assistantId}" not found.`);
|
|
261
|
+
return assistant;
|
|
262
|
+
};
|
|
263
|
+
return {
|
|
264
|
+
async registerGraphAssistants() {
|
|
265
|
+
const registered = [];
|
|
266
|
+
for (const graphId of deps.graphs.ids) {
|
|
267
|
+
const existing = await deps.store.assistants.get(graphId);
|
|
268
|
+
registered.push(
|
|
269
|
+
existing ?? await deps.store.assistants.create({ graph_id: graphId, assistant_id: graphId })
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
return registered;
|
|
273
|
+
},
|
|
274
|
+
get: requireAssistant,
|
|
275
|
+
list: () => deps.store.assistants.list(),
|
|
276
|
+
async search(query) {
|
|
277
|
+
const all = await deps.store.assistants.list();
|
|
278
|
+
const filtered = query.graph_id ? all.filter((assistant) => assistant.graph_id === query.graph_id) : all;
|
|
279
|
+
const offset = query.offset ?? 0;
|
|
280
|
+
return filtered.slice(offset, query.limit === void 0 ? void 0 : offset + query.limit);
|
|
281
|
+
},
|
|
282
|
+
async schemas(assistantId) {
|
|
283
|
+
const assistant = await requireAssistant(assistantId);
|
|
284
|
+
return deps.graphs.schemas(assistant.graph_id);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// src/deps.ts
|
|
290
|
+
var noopLogger = {
|
|
291
|
+
debug: () => {
|
|
292
|
+
},
|
|
293
|
+
info: () => {
|
|
294
|
+
},
|
|
295
|
+
warn: () => {
|
|
296
|
+
},
|
|
297
|
+
error: () => {
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
function resolveDeps(deps) {
|
|
301
|
+
return {
|
|
302
|
+
...deps,
|
|
303
|
+
clock: deps.clock ?? (() => /* @__PURE__ */ new Date()),
|
|
304
|
+
logger: deps.logger ?? noopLogger
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// src/runs/cancellation.ts
|
|
309
|
+
var RunControlRegistry = class {
|
|
310
|
+
#entries = /* @__PURE__ */ new Map();
|
|
311
|
+
/** Start tracking a run and return its abort signal + reason box. */
|
|
312
|
+
register(runId) {
|
|
313
|
+
const controller = new AbortController();
|
|
314
|
+
const reason = { current: null };
|
|
315
|
+
const abort = (next) => {
|
|
316
|
+
if (reason.current === null) reason.current = next;
|
|
317
|
+
controller.abort();
|
|
318
|
+
};
|
|
319
|
+
this.#entries.set(runId, { controller, reason, abort });
|
|
320
|
+
return { signal: controller.signal, reason, abort };
|
|
321
|
+
}
|
|
322
|
+
/** Abort a tracked run with a reason. Returns false if the run isn't currently executing. */
|
|
323
|
+
abort(runId, reason) {
|
|
324
|
+
const entry = this.#entries.get(runId);
|
|
325
|
+
if (!entry) return false;
|
|
326
|
+
entry.abort(reason);
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
/** Stop tracking a run once it has settled. */
|
|
330
|
+
clear(runId) {
|
|
331
|
+
this.#entries.delete(runId);
|
|
332
|
+
}
|
|
333
|
+
/** Whether a run is currently being tracked (i.e. executing). */
|
|
334
|
+
isTracking(runId) {
|
|
335
|
+
return this.#entries.has(runId);
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
// src/runs/thread-locks.ts
|
|
340
|
+
var ThreadLocks = class {
|
|
341
|
+
#tails = /* @__PURE__ */ new Map();
|
|
342
|
+
/** Run `task` after any in-flight task for `key` has settled, serializing access per key. */
|
|
343
|
+
async run(key, task) {
|
|
344
|
+
const previous = this.#tails.get(key) ?? Promise.resolve();
|
|
345
|
+
const result = previous.then(task, task);
|
|
346
|
+
const tail = result.then(
|
|
347
|
+
() => void 0,
|
|
348
|
+
() => void 0
|
|
349
|
+
);
|
|
350
|
+
this.#tails.set(key, tail);
|
|
351
|
+
try {
|
|
352
|
+
return await result;
|
|
353
|
+
} finally {
|
|
354
|
+
if (this.#tails.get(key) === tail) this.#tails.delete(key);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
// src/context.ts
|
|
360
|
+
function createContext(deps) {
|
|
361
|
+
return {
|
|
362
|
+
deps: resolveDeps(deps),
|
|
363
|
+
control: new RunControlRegistry(),
|
|
364
|
+
locks: new ThreadLocks()
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// src/runs/run-service.ts
|
|
369
|
+
import {
|
|
370
|
+
isTerminalRunStatus as isTerminalRunStatus3,
|
|
371
|
+
SkeinHttpError as SkeinHttpError5
|
|
372
|
+
} from "@skein-js/core";
|
|
373
|
+
|
|
374
|
+
// src/runs/run-engine.ts
|
|
375
|
+
import {
|
|
376
|
+
isTerminalRunStatus as isTerminalRunStatus2,
|
|
377
|
+
SkeinHttpError as SkeinHttpError4
|
|
378
|
+
} from "@skein-js/core";
|
|
379
|
+
|
|
380
|
+
// src/normalize-error.ts
|
|
381
|
+
import { isSkeinHttpError, SkeinHttpError as SkeinHttpError3 } from "@skein-js/core";
|
|
382
|
+
function serializeError(error) {
|
|
383
|
+
if (error instanceof Error) {
|
|
384
|
+
return { name: error.name, message: error.message };
|
|
385
|
+
}
|
|
386
|
+
return { name: "Error", message: String(error) };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// src/sse/run-frame-stream.ts
|
|
390
|
+
var STREAM_MODES = [
|
|
391
|
+
"values",
|
|
392
|
+
"updates",
|
|
393
|
+
"messages",
|
|
394
|
+
"messages-tuple",
|
|
395
|
+
"custom",
|
|
396
|
+
"events",
|
|
397
|
+
"debug"
|
|
398
|
+
];
|
|
399
|
+
function isStreamMode(value) {
|
|
400
|
+
return typeof value === "string" && STREAM_MODES.includes(value);
|
|
401
|
+
}
|
|
402
|
+
function chunkToFrameBody(chunk) {
|
|
403
|
+
if (Array.isArray(chunk) && chunk.length === 2 && isStreamMode(chunk[0])) {
|
|
404
|
+
return { event: chunk[0], data: chunk[1] };
|
|
405
|
+
}
|
|
406
|
+
return { event: "updates", data: chunk };
|
|
407
|
+
}
|
|
408
|
+
function toRunFrame(seq, body) {
|
|
409
|
+
return { seq, event: body.event, data: body.data };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// src/store/skein-base-store.ts
|
|
413
|
+
import {
|
|
414
|
+
BaseStore
|
|
415
|
+
} from "@langchain/langgraph";
|
|
416
|
+
function toLangGraphItem(item) {
|
|
417
|
+
return {
|
|
418
|
+
namespace: item.namespace,
|
|
419
|
+
key: item.key,
|
|
420
|
+
value: item.value,
|
|
421
|
+
createdAt: new Date(item.createdAt),
|
|
422
|
+
updatedAt: new Date(item.updatedAt)
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
function toLangGraphSearchItem(item) {
|
|
426
|
+
const base = toLangGraphItem(item);
|
|
427
|
+
return item.score === void 0 ? base : { ...base, score: item.score };
|
|
428
|
+
}
|
|
429
|
+
function prefixFromListOperation(operation) {
|
|
430
|
+
const prefixCondition = operation.matchConditions?.find(
|
|
431
|
+
(condition) => condition.matchType === "prefix"
|
|
432
|
+
);
|
|
433
|
+
if (!prefixCondition) return void 0;
|
|
434
|
+
return prefixCondition.path.every((segment) => segment !== "*") ? prefixCondition.path : void 0;
|
|
435
|
+
}
|
|
436
|
+
var SkeinBaseStore = class extends BaseStore {
|
|
437
|
+
constructor(repo) {
|
|
438
|
+
super();
|
|
439
|
+
this.repo = repo;
|
|
440
|
+
}
|
|
441
|
+
repo;
|
|
442
|
+
async get(namespace, key) {
|
|
443
|
+
const item = await this.repo.get(namespace, key);
|
|
444
|
+
return item ? toLangGraphItem(item) : null;
|
|
445
|
+
}
|
|
446
|
+
async put(namespace, key, value) {
|
|
447
|
+
await this.repo.put(namespace, key, value);
|
|
448
|
+
}
|
|
449
|
+
async delete(namespace, key) {
|
|
450
|
+
await this.repo.delete(namespace, key);
|
|
451
|
+
}
|
|
452
|
+
async search(namespacePrefix, options) {
|
|
453
|
+
const query = { prefix: namespacePrefix, limit: options?.limit ?? 10 };
|
|
454
|
+
if (options?.query !== void 0) query.query = options.query;
|
|
455
|
+
if (options?.offset !== void 0) query.offset = options.offset;
|
|
456
|
+
const items = await this.repo.search(query);
|
|
457
|
+
return items.map(toLangGraphSearchItem);
|
|
458
|
+
}
|
|
459
|
+
async listNamespaces(options) {
|
|
460
|
+
const all = await this.repo.listNamespaces(options?.prefix);
|
|
461
|
+
const offset = options?.offset ?? 0;
|
|
462
|
+
const end = options?.limit === void 0 ? void 0 : offset + options.limit;
|
|
463
|
+
return all.slice(offset, end);
|
|
464
|
+
}
|
|
465
|
+
// BaseStore's only abstract method. The convenience methods above are overridden to hit the repo
|
|
466
|
+
// directly, so this dispatcher can safely call them without recursing back through `batch`.
|
|
467
|
+
async batch(operations) {
|
|
468
|
+
const results = await Promise.all(operations.map((operation) => this.runOperation(operation)));
|
|
469
|
+
return results;
|
|
470
|
+
}
|
|
471
|
+
runOperation(operation) {
|
|
472
|
+
if ("namespacePrefix" in operation) {
|
|
473
|
+
const search = operation;
|
|
474
|
+
return this.search(search.namespacePrefix, search);
|
|
475
|
+
}
|
|
476
|
+
if ("value" in operation) {
|
|
477
|
+
const put = operation;
|
|
478
|
+
return put.value === null ? this.delete(put.namespace, put.key) : this.put(put.namespace, put.key, put.value);
|
|
479
|
+
}
|
|
480
|
+
if ("key" in operation) {
|
|
481
|
+
const get = operation;
|
|
482
|
+
return this.get(get.namespace, get.key);
|
|
483
|
+
}
|
|
484
|
+
return this.listNamespaces({ prefix: prefixFromListOperation(operation) });
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
// src/runs/run-status.ts
|
|
489
|
+
import { isTerminalRunStatus } from "@skein-js/core";
|
|
490
|
+
function threadStatusForRun(status) {
|
|
491
|
+
switch (status) {
|
|
492
|
+
case "running":
|
|
493
|
+
return "busy";
|
|
494
|
+
case "interrupted":
|
|
495
|
+
return "interrupted";
|
|
496
|
+
case "error":
|
|
497
|
+
case "timeout":
|
|
498
|
+
return "error";
|
|
499
|
+
// pending (not yet started) and success both leave the thread idle.
|
|
500
|
+
case "pending":
|
|
501
|
+
case "success":
|
|
502
|
+
return "idle";
|
|
503
|
+
default:
|
|
504
|
+
return "idle";
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// src/threads/thread-mirror.ts
|
|
509
|
+
function isInterruptedSnapshot(snapshot) {
|
|
510
|
+
return snapshot.next.length > 0;
|
|
511
|
+
}
|
|
512
|
+
function collectInterrupts(snapshot) {
|
|
513
|
+
const byTask = {};
|
|
514
|
+
for (const task of snapshot.tasks) {
|
|
515
|
+
if (task.interrupts && task.interrupts.length > 0) {
|
|
516
|
+
byTask[task.id] = task.interrupts;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return byTask;
|
|
520
|
+
}
|
|
521
|
+
function runStatusForSnapshot(snapshot) {
|
|
522
|
+
return isInterruptedSnapshot(snapshot) ? "interrupted" : "success";
|
|
523
|
+
}
|
|
524
|
+
function snapshotToThreadUpdate(snapshot, runStatus) {
|
|
525
|
+
return {
|
|
526
|
+
values: snapshot.values,
|
|
527
|
+
interrupts: collectInterrupts(snapshot),
|
|
528
|
+
status: threadStatusForRun(runStatus)
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
function toCheckpoint(config) {
|
|
532
|
+
const configurable = config?.configurable;
|
|
533
|
+
if (!configurable) return null;
|
|
534
|
+
const threadId = configurable["thread_id"];
|
|
535
|
+
if (typeof threadId !== "string") return null;
|
|
536
|
+
return {
|
|
537
|
+
thread_id: threadId,
|
|
538
|
+
checkpoint_ns: configurable["checkpoint_ns"] ?? "",
|
|
539
|
+
checkpoint_id: configurable["checkpoint_id"],
|
|
540
|
+
checkpoint_map: configurable["checkpoint_map"]
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
function toThreadTask(task) {
|
|
544
|
+
return {
|
|
545
|
+
id: task.id,
|
|
546
|
+
name: task.name,
|
|
547
|
+
result: task.result,
|
|
548
|
+
error: task.error === void 0 ? null : String(task.error),
|
|
549
|
+
interrupts: task.interrupts ?? [],
|
|
550
|
+
checkpoint: null,
|
|
551
|
+
state: null
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
function snapshotToThreadState(snapshot) {
|
|
555
|
+
return {
|
|
556
|
+
values: snapshot.values,
|
|
557
|
+
next: [...snapshot.next],
|
|
558
|
+
checkpoint: toCheckpoint(snapshot.config) ?? {
|
|
559
|
+
thread_id: "",
|
|
560
|
+
checkpoint_ns: "",
|
|
561
|
+
checkpoint_id: void 0,
|
|
562
|
+
checkpoint_map: void 0
|
|
563
|
+
},
|
|
564
|
+
metadata: snapshot.metadata ?? {},
|
|
565
|
+
created_at: snapshot.createdAt ?? null,
|
|
566
|
+
parent_checkpoint: toCheckpoint(snapshot.parentConfig),
|
|
567
|
+
tasks: snapshot.tasks.map(toThreadTask)
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// src/runs/run-input.ts
|
|
572
|
+
import { Command } from "@langchain/langgraph";
|
|
573
|
+
function toGraphMode(mode) {
|
|
574
|
+
if (mode === "messages-tuple") return "messages";
|
|
575
|
+
if (mode === "events") return "updates";
|
|
576
|
+
return mode;
|
|
577
|
+
}
|
|
578
|
+
function normalizeModes(mode) {
|
|
579
|
+
const requested = mode === void 0 ? [] : Array.isArray(mode) ? mode : [mode];
|
|
580
|
+
const mapped = requested.map(toGraphMode);
|
|
581
|
+
const deduped = [...new Set(mapped)];
|
|
582
|
+
return deduped.length > 0 ? deduped : ["values"];
|
|
583
|
+
}
|
|
584
|
+
function toGraphInput(kwargs) {
|
|
585
|
+
if (kwargs.command) {
|
|
586
|
+
return new Command(kwargs.command);
|
|
587
|
+
}
|
|
588
|
+
return kwargs.input ?? null;
|
|
589
|
+
}
|
|
590
|
+
var RESERVED_CONFIGURABLE_KEYS = /* @__PURE__ */ new Set([
|
|
591
|
+
"thread_id",
|
|
592
|
+
"run_id",
|
|
593
|
+
"checkpoint_id",
|
|
594
|
+
"checkpoint_ns",
|
|
595
|
+
"checkpoint_map",
|
|
596
|
+
"checkpoint"
|
|
597
|
+
]);
|
|
598
|
+
function sanitizeConfigurable(configurable) {
|
|
599
|
+
if (!configurable) return {};
|
|
600
|
+
const clean = {};
|
|
601
|
+
for (const [key, value] of Object.entries(configurable)) {
|
|
602
|
+
if (RESERVED_CONFIGURABLE_KEYS.has(key) || key.startsWith("__")) continue;
|
|
603
|
+
clean[key] = value;
|
|
604
|
+
}
|
|
605
|
+
return clean;
|
|
606
|
+
}
|
|
607
|
+
function toGraphCallOptions(kwargs, threadId, signal) {
|
|
608
|
+
const options = {
|
|
609
|
+
// Drop server-owned keys from the client's configurable, then force this thread's id.
|
|
610
|
+
configurable: { ...sanitizeConfigurable(kwargs.config?.configurable), thread_id: threadId },
|
|
611
|
+
streamMode: normalizeModes(kwargs.stream_mode),
|
|
612
|
+
signal
|
|
613
|
+
};
|
|
614
|
+
if (kwargs.context !== void 0) options.context = kwargs.context;
|
|
615
|
+
const recursionLimit = kwargs.config?.recursion_limit;
|
|
616
|
+
if (typeof recursionLimit === "number") options.recursionLimit = recursionLimit;
|
|
617
|
+
if (kwargs.interrupt_before !== void 0) options.interruptBefore = kwargs.interrupt_before;
|
|
618
|
+
if (kwargs.interrupt_after !== void 0) options.interruptAfter = kwargs.interrupt_after;
|
|
619
|
+
return options;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// src/runs/run-log.ts
|
|
623
|
+
function isRecord(value) {
|
|
624
|
+
return typeof value === "object" && value !== null;
|
|
625
|
+
}
|
|
626
|
+
function messageType(node) {
|
|
627
|
+
if (typeof node["type"] === "string") return node["type"];
|
|
628
|
+
const getType = node["_getType"];
|
|
629
|
+
if (typeof getType === "function") {
|
|
630
|
+
try {
|
|
631
|
+
const type = getType.call(node);
|
|
632
|
+
return typeof type === "string" ? type : void 0;
|
|
633
|
+
} catch {
|
|
634
|
+
return void 0;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
return void 0;
|
|
638
|
+
}
|
|
639
|
+
function extractToolActivity(data) {
|
|
640
|
+
const calls = [];
|
|
641
|
+
const results = [];
|
|
642
|
+
const seen = /* @__PURE__ */ new Set();
|
|
643
|
+
const visit = (node, depth) => {
|
|
644
|
+
if (depth > 6 || !isRecord(node) || seen.has(node)) return;
|
|
645
|
+
seen.add(node);
|
|
646
|
+
const toolCalls = node["tool_calls"];
|
|
647
|
+
if (Array.isArray(toolCalls)) {
|
|
648
|
+
for (const call of toolCalls) {
|
|
649
|
+
if (isRecord(call) && typeof call["name"] === "string" && call["name"].length > 0) {
|
|
650
|
+
calls.push({ name: call["name"], id: typeof call["id"] === "string" ? call["id"] : void 0 });
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
if (messageType(node) === "tool") {
|
|
655
|
+
results.push({
|
|
656
|
+
name: typeof node["name"] === "string" ? node["name"] : "tool",
|
|
657
|
+
id: typeof node["tool_call_id"] === "string" ? node["tool_call_id"] : void 0
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
for (const value of Object.values(node)) {
|
|
661
|
+
if (Array.isArray(value)) {
|
|
662
|
+
for (const item of value) visit(item, depth + 1);
|
|
663
|
+
} else if (isRecord(value)) {
|
|
664
|
+
visit(value, depth + 1);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
try {
|
|
669
|
+
visit(data, 0);
|
|
670
|
+
} catch {
|
|
671
|
+
}
|
|
672
|
+
return { calls, results };
|
|
673
|
+
}
|
|
674
|
+
function describeInterrupts(snapshot) {
|
|
675
|
+
const prompts = [];
|
|
676
|
+
try {
|
|
677
|
+
for (const task of snapshot.tasks ?? []) {
|
|
678
|
+
for (const interrupt of task.interrupts ?? []) {
|
|
679
|
+
const value = interrupt.value;
|
|
680
|
+
prompts.push(typeof value === "string" ? value : JSON.stringify(value));
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
} catch {
|
|
684
|
+
}
|
|
685
|
+
return prompts;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// src/runs/run-engine.ts
|
|
689
|
+
async function resolveGraph(deps, graphId, kwargs) {
|
|
690
|
+
const resolved = await deps.graphs.load(graphId);
|
|
691
|
+
const graph = typeof resolved === "function" ? await resolved({ configurable: kwargs.config?.configurable }) : resolved;
|
|
692
|
+
graph.checkpointer = deps.checkpointer;
|
|
693
|
+
graph.store = new SkeinBaseStore(deps.store.store);
|
|
694
|
+
return graph;
|
|
695
|
+
}
|
|
696
|
+
async function finalizeRun(deps, runId, status) {
|
|
697
|
+
const fresh = await deps.store.runs.get(runId);
|
|
698
|
+
if (!fresh) return status;
|
|
699
|
+
if (isTerminalRunStatus2(fresh.status)) return fresh.status;
|
|
700
|
+
await deps.store.runs.setStatus(runId, status);
|
|
701
|
+
return status;
|
|
702
|
+
}
|
|
703
|
+
async function mirrorThread(deps, threadId, patch) {
|
|
704
|
+
const thread = await deps.store.threads.get(threadId);
|
|
705
|
+
if (!thread) return;
|
|
706
|
+
let effective = patch;
|
|
707
|
+
if (patch.status && patch.status !== "error" && patch.metadata === void 0 && thread.metadata != null && "error" in thread.metadata) {
|
|
708
|
+
const cleared = { ...thread.metadata };
|
|
709
|
+
delete cleared["error"];
|
|
710
|
+
effective = { ...patch, metadata: cleared };
|
|
711
|
+
}
|
|
712
|
+
await deps.store.threads.update(threadId, effective);
|
|
713
|
+
}
|
|
714
|
+
async function mirrorThreadStatus(deps, threadId, status) {
|
|
715
|
+
await mirrorThread(deps, threadId, { status });
|
|
716
|
+
}
|
|
717
|
+
async function mirrorThreadError(deps, threadId, message) {
|
|
718
|
+
const thread = await deps.store.threads.get(threadId);
|
|
719
|
+
if (!thread) return;
|
|
720
|
+
const metadata = { ...thread.metadata, error: message };
|
|
721
|
+
await deps.store.threads.update(threadId, { status: "error", metadata });
|
|
722
|
+
}
|
|
723
|
+
async function executeRun(deps, exec) {
|
|
724
|
+
const { run, kwargs, control } = exec;
|
|
725
|
+
const runId = run.run_id;
|
|
726
|
+
const threadId = run.thread_id;
|
|
727
|
+
let seq = 0;
|
|
728
|
+
const startedAt = deps.clock().getTime();
|
|
729
|
+
const loggedTools = /* @__PURE__ */ new Set();
|
|
730
|
+
const logToolActivity = (data) => {
|
|
731
|
+
const { calls, results } = extractToolActivity(data);
|
|
732
|
+
for (const call of calls) {
|
|
733
|
+
const key = `call:${call.id ?? call.name}`;
|
|
734
|
+
if (loggedTools.has(key)) continue;
|
|
735
|
+
loggedTools.add(key);
|
|
736
|
+
deps.logger.info(`run ${runId} \u2192 tool call: ${call.name}`);
|
|
737
|
+
}
|
|
738
|
+
for (const result of results) {
|
|
739
|
+
const key = `result:${result.id ?? result.name}`;
|
|
740
|
+
if (loggedTools.has(key)) continue;
|
|
741
|
+
loggedTools.add(key);
|
|
742
|
+
deps.logger.info(`run ${runId} \u2190 tool result: ${result.name}`);
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
const logFinished = (status) => {
|
|
746
|
+
if (!deps.logRunActivity) return;
|
|
747
|
+
deps.logger.info(
|
|
748
|
+
`run ${runId} ${status} in ${deps.clock().getTime() - startedAt}ms (${seq} frames)`
|
|
749
|
+
);
|
|
750
|
+
};
|
|
751
|
+
const timer = deps.runTimeoutMs !== void 0 ? setTimeout(() => control.abort("timeout"), deps.runTimeoutMs) : void 0;
|
|
752
|
+
try {
|
|
753
|
+
const current = await deps.store.runs.get(runId);
|
|
754
|
+
if (current && isTerminalRunStatus2(current.status)) {
|
|
755
|
+
return { status: current.status, values: {} };
|
|
756
|
+
}
|
|
757
|
+
await deps.store.runs.setStatus(runId, "running");
|
|
758
|
+
await mirrorThreadStatus(deps, threadId, "busy");
|
|
759
|
+
if (deps.logRunActivity) {
|
|
760
|
+
deps.logger.info(`run ${runId} started \xB7 assistant=${run.assistant_id} thread=${threadId}`);
|
|
761
|
+
}
|
|
762
|
+
const assistant = await deps.store.assistants.get(run.assistant_id);
|
|
763
|
+
if (!assistant) {
|
|
764
|
+
throw SkeinHttpError4.notFound(`Assistant "${run.assistant_id}" not found.`);
|
|
765
|
+
}
|
|
766
|
+
const graph = await resolveGraph(deps, assistant.graph_id, kwargs);
|
|
767
|
+
const input = toGraphInput(kwargs);
|
|
768
|
+
const options = toGraphCallOptions(kwargs, threadId, control.signal);
|
|
769
|
+
const stream = await graph.stream(input, options);
|
|
770
|
+
for await (const chunk of stream) {
|
|
771
|
+
seq += 1;
|
|
772
|
+
const body = chunkToFrameBody(chunk);
|
|
773
|
+
await deps.bus.publish(runId, toRunFrame(seq, body));
|
|
774
|
+
if (deps.logRunActivity) logToolActivity(body.data);
|
|
775
|
+
}
|
|
776
|
+
if (control.signal.aborted) {
|
|
777
|
+
const timedOut = control.reason.current === "timeout";
|
|
778
|
+
const finalStatus2 = await finalizeRun(deps, runId, timedOut ? "timeout" : "error");
|
|
779
|
+
if (finalStatus2 === "timeout") await mirrorThreadError(deps, threadId, "Run timed out.");
|
|
780
|
+
else if (finalStatus2 === "error") await mirrorThreadStatus(deps, threadId, "idle");
|
|
781
|
+
logFinished(finalStatus2);
|
|
782
|
+
return { status: finalStatus2, values: {} };
|
|
783
|
+
}
|
|
784
|
+
const snapshot = await graph.getState({ configurable: { thread_id: threadId } });
|
|
785
|
+
const computed = runStatusForSnapshot(snapshot);
|
|
786
|
+
const finalStatus = await finalizeRun(deps, runId, computed);
|
|
787
|
+
if (finalStatus === computed) {
|
|
788
|
+
await mirrorThread(deps, threadId, snapshotToThreadUpdate(snapshot, finalStatus));
|
|
789
|
+
} else {
|
|
790
|
+
await mirrorThreadStatus(deps, threadId, finalStatus === "error" ? "error" : "idle");
|
|
791
|
+
}
|
|
792
|
+
if (deps.logRunActivity && finalStatus === "interrupted") {
|
|
793
|
+
const prompts = describeInterrupts(snapshot);
|
|
794
|
+
deps.logger.info(
|
|
795
|
+
`run ${runId} interrupted${prompts.length ? ` \xB7 awaiting: ${prompts.join("; ")}` : ""}`
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
logFinished(finalStatus);
|
|
799
|
+
return { status: finalStatus, values: snapshot.values };
|
|
800
|
+
} catch (error) {
|
|
801
|
+
const reason = control.reason.current;
|
|
802
|
+
if (reason === "timeout") {
|
|
803
|
+
const finalStatus2 = await finalizeRun(deps, runId, "timeout");
|
|
804
|
+
if (finalStatus2 === "timeout") await mirrorThreadError(deps, threadId, "Run timed out.");
|
|
805
|
+
logFinished(finalStatus2);
|
|
806
|
+
return { status: finalStatus2, values: {} };
|
|
807
|
+
}
|
|
808
|
+
if (reason === "cancel" || control.signal.aborted) {
|
|
809
|
+
const finalStatus2 = await finalizeRun(deps, runId, "error");
|
|
810
|
+
if (finalStatus2 === "error") await mirrorThreadStatus(deps, threadId, "idle");
|
|
811
|
+
logFinished(finalStatus2);
|
|
812
|
+
return { status: finalStatus2, values: {} };
|
|
813
|
+
}
|
|
814
|
+
const serialized = serializeError(error);
|
|
815
|
+
seq += 1;
|
|
816
|
+
await deps.bus.publish(runId, { seq, event: "error", data: serialized });
|
|
817
|
+
const finalStatus = await finalizeRun(deps, runId, "error");
|
|
818
|
+
if (finalStatus === "error") await mirrorThreadError(deps, threadId, serialized.message);
|
|
819
|
+
if (deps.logRunActivity) deps.logger.error(`run ${runId} error: ${serialized.message}`);
|
|
820
|
+
logFinished(finalStatus);
|
|
821
|
+
return { status: finalStatus, values: {} };
|
|
822
|
+
} finally {
|
|
823
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
824
|
+
await deps.bus.close(runId);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// src/runs/run-service.ts
|
|
829
|
+
function toKwargs(input) {
|
|
830
|
+
const kwargs = {};
|
|
831
|
+
if (input.input !== void 0) kwargs.input = input.input;
|
|
832
|
+
if (input.command !== void 0) kwargs.command = input.command;
|
|
833
|
+
if (input.config !== void 0) kwargs.config = input.config;
|
|
834
|
+
if (input.context !== void 0) kwargs.context = input.context;
|
|
835
|
+
if (input.stream_mode !== void 0) kwargs.stream_mode = input.stream_mode;
|
|
836
|
+
if (input.interrupt_before !== void 0) kwargs.interrupt_before = input.interrupt_before;
|
|
837
|
+
if (input.interrupt_after !== void 0) kwargs.interrupt_after = input.interrupt_after;
|
|
838
|
+
return kwargs;
|
|
839
|
+
}
|
|
840
|
+
function createRunService(ctx) {
|
|
841
|
+
const { deps, control, locks } = ctx;
|
|
842
|
+
const requireThread = async (threadId) => {
|
|
843
|
+
if (!await deps.store.threads.get(threadId)) {
|
|
844
|
+
throw SkeinHttpError5.notFound(`Thread "${threadId}" not found.`);
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
const requireAssistant = async (assistantId) => {
|
|
848
|
+
const assistant = await deps.store.assistants.get(assistantId);
|
|
849
|
+
if (!assistant) throw SkeinHttpError5.notFound(`Assistant "${assistantId}" not found.`);
|
|
850
|
+
return assistant.assistant_id;
|
|
851
|
+
};
|
|
852
|
+
const ensureThread = async (threadId) => {
|
|
853
|
+
if (threadId === void 0) return (await deps.store.threads.create()).thread_id;
|
|
854
|
+
const existing = await deps.store.threads.get(threadId);
|
|
855
|
+
return (existing ?? await deps.store.threads.create({ thread_id: threadId })).thread_id;
|
|
856
|
+
};
|
|
857
|
+
const createPendingRun = async (input, threadId, assistantId, kwargs) => {
|
|
858
|
+
return locks.run(threadId, async () => {
|
|
859
|
+
const strategy = input.multitask_strategy ?? "reject";
|
|
860
|
+
if (await deps.store.runs.hasActiveRun(threadId)) {
|
|
861
|
+
if (strategy !== "reject") {
|
|
862
|
+
deps.logger.warn(`multitask_strategy "${strategy}" treated as reject (MVP).`);
|
|
863
|
+
}
|
|
864
|
+
throw SkeinHttpError5.conflict(`Thread "${threadId}" already has an active run.`, {
|
|
865
|
+
code: "thread_busy"
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
return deps.store.runs.create({
|
|
869
|
+
thread_id: threadId,
|
|
870
|
+
assistant_id: assistantId,
|
|
871
|
+
status: "pending",
|
|
872
|
+
metadata: input.metadata,
|
|
873
|
+
multitask_strategy: strategy,
|
|
874
|
+
kwargs
|
|
875
|
+
});
|
|
876
|
+
});
|
|
877
|
+
};
|
|
878
|
+
const runInline = (run, kwargs) => {
|
|
879
|
+
const runControl = control.register(run.run_id);
|
|
880
|
+
const done = executeRun(deps, { run, kwargs, control: runControl }).finally(
|
|
881
|
+
() => control.clear(run.run_id)
|
|
882
|
+
);
|
|
883
|
+
return done;
|
|
884
|
+
};
|
|
885
|
+
return {
|
|
886
|
+
async createWait(input) {
|
|
887
|
+
const assistantId = await requireAssistant(input.assistant_id);
|
|
888
|
+
const threadId = await ensureThread(input.thread_id);
|
|
889
|
+
const kwargs = toKwargs(input);
|
|
890
|
+
const run = await createPendingRun(input, threadId, assistantId, kwargs);
|
|
891
|
+
const outcome = await runInline(run, kwargs);
|
|
892
|
+
return outcome.values;
|
|
893
|
+
},
|
|
894
|
+
async createStream(input) {
|
|
895
|
+
const assistantId = await requireAssistant(input.assistant_id);
|
|
896
|
+
const threadId = await ensureThread(input.thread_id);
|
|
897
|
+
const kwargs = toKwargs(input);
|
|
898
|
+
const run = await createPendingRun(input, threadId, assistantId, kwargs);
|
|
899
|
+
void runInline(run, kwargs).catch(
|
|
900
|
+
(error) => deps.logger.error("stream run failed", error)
|
|
901
|
+
);
|
|
902
|
+
return { runId: run.run_id, frames: deps.bus.subscribe(run.run_id, 0) };
|
|
903
|
+
},
|
|
904
|
+
async createBackground(threadId, input) {
|
|
905
|
+
const assistantId = await requireAssistant(input.assistant_id);
|
|
906
|
+
await requireThread(threadId);
|
|
907
|
+
const kwargs = toKwargs(input);
|
|
908
|
+
const run = await createPendingRun(
|
|
909
|
+
{ ...input, thread_id: threadId },
|
|
910
|
+
threadId,
|
|
911
|
+
assistantId,
|
|
912
|
+
kwargs
|
|
913
|
+
);
|
|
914
|
+
await deps.queue.enqueue({ run_id: run.run_id, thread_id: threadId });
|
|
915
|
+
return run;
|
|
916
|
+
},
|
|
917
|
+
async get(runId) {
|
|
918
|
+
const run = await deps.store.runs.get(runId);
|
|
919
|
+
if (!run) throw SkeinHttpError5.notFound(`Run "${runId}" not found.`);
|
|
920
|
+
return run;
|
|
921
|
+
},
|
|
922
|
+
async listByThread(threadId) {
|
|
923
|
+
await requireThread(threadId);
|
|
924
|
+
return deps.store.runs.listByThread(threadId);
|
|
925
|
+
},
|
|
926
|
+
async cancel(runId) {
|
|
927
|
+
const run = await deps.store.runs.get(runId);
|
|
928
|
+
if (!run) throw SkeinHttpError5.notFound(`Run "${runId}" not found.`);
|
|
929
|
+
if (isTerminalRunStatus3(run.status)) return run;
|
|
930
|
+
if (run.status === "pending") {
|
|
931
|
+
await deps.store.runs.setStatus(runId, "error");
|
|
932
|
+
await deps.store.threads.update(run.thread_id, { status: "idle" });
|
|
933
|
+
await deps.bus.close(runId);
|
|
934
|
+
control.abort(runId, "cancel");
|
|
935
|
+
} else {
|
|
936
|
+
await deps.store.runs.setStatus(runId, "error");
|
|
937
|
+
await deps.store.threads.update(run.thread_id, { status: "idle" });
|
|
938
|
+
control.abort(runId, "cancel");
|
|
939
|
+
}
|
|
940
|
+
return await deps.store.runs.get(runId) ?? run;
|
|
941
|
+
},
|
|
942
|
+
async delete(runId) {
|
|
943
|
+
if (!await deps.store.runs.get(runId)) {
|
|
944
|
+
throw SkeinHttpError5.notFound(`Run "${runId}" not found.`);
|
|
945
|
+
}
|
|
946
|
+
control.abort(runId, "cancel");
|
|
947
|
+
await deps.store.runs.delete(runId);
|
|
948
|
+
},
|
|
949
|
+
async join(runId, afterSeq = 0) {
|
|
950
|
+
if (!await deps.store.runs.get(runId)) {
|
|
951
|
+
throw SkeinHttpError5.notFound(`Run "${runId}" not found.`);
|
|
952
|
+
}
|
|
953
|
+
return deps.bus.subscribe(runId, afterSeq);
|
|
954
|
+
},
|
|
955
|
+
async finalStatus(runId) {
|
|
956
|
+
const run = await deps.store.runs.get(runId);
|
|
957
|
+
return run?.status ?? null;
|
|
958
|
+
}
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// src/store/store-service.ts
|
|
963
|
+
import { SkeinHttpError as SkeinHttpError6 } from "@skein-js/core";
|
|
964
|
+
function createStoreService(deps) {
|
|
965
|
+
return {
|
|
966
|
+
put: (namespace, key, value) => deps.store.store.put(namespace, key, value),
|
|
967
|
+
async get(namespace, key) {
|
|
968
|
+
const item = await deps.store.store.get(namespace, key);
|
|
969
|
+
if (!item) {
|
|
970
|
+
throw SkeinHttpError6.notFound(
|
|
971
|
+
`Store item "${key}" not found in namespace [${namespace.join(", ")}].`
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
return item;
|
|
975
|
+
},
|
|
976
|
+
delete: (namespace, key) => deps.store.store.delete(namespace, key),
|
|
977
|
+
search: (query) => deps.store.store.search(query),
|
|
978
|
+
listNamespaces: (prefix) => deps.store.store.listNamespaces(prefix)
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// src/threads/thread-service.ts
|
|
983
|
+
import {
|
|
984
|
+
isTerminalRunStatus as isTerminalRunStatus4,
|
|
985
|
+
SkeinHttpError as SkeinHttpError7
|
|
986
|
+
} from "@skein-js/core";
|
|
987
|
+
function emptyThreadState(threadId) {
|
|
988
|
+
return {
|
|
989
|
+
values: {},
|
|
990
|
+
next: [],
|
|
991
|
+
checkpoint: {
|
|
992
|
+
thread_id: threadId,
|
|
993
|
+
checkpoint_ns: "",
|
|
994
|
+
checkpoint_id: void 0,
|
|
995
|
+
checkpoint_map: void 0
|
|
996
|
+
},
|
|
997
|
+
metadata: {},
|
|
998
|
+
created_at: null,
|
|
999
|
+
parent_checkpoint: null,
|
|
1000
|
+
tasks: []
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
function createThreadService(ctx) {
|
|
1004
|
+
const { deps, control } = ctx;
|
|
1005
|
+
const requireThread = async (threadId) => {
|
|
1006
|
+
const thread = await deps.store.threads.get(threadId);
|
|
1007
|
+
if (!thread) throw SkeinHttpError7.notFound(`Thread "${threadId}" not found.`);
|
|
1008
|
+
return thread;
|
|
1009
|
+
};
|
|
1010
|
+
const readHistory = async (threadId, options) => {
|
|
1011
|
+
await requireThread(threadId);
|
|
1012
|
+
const runs = await deps.store.runs.listByThread(threadId);
|
|
1013
|
+
const latest = [...runs].sort((a, b) => b.created_at.localeCompare(a.created_at))[0];
|
|
1014
|
+
if (!latest) return [];
|
|
1015
|
+
const assistant = await deps.store.assistants.get(latest.assistant_id);
|
|
1016
|
+
if (!assistant) return [];
|
|
1017
|
+
const resolved = await deps.graphs.load(assistant.graph_id);
|
|
1018
|
+
let graph;
|
|
1019
|
+
if (typeof resolved === "function") {
|
|
1020
|
+
const kwargs = await deps.store.runs.getKwargs(latest.run_id);
|
|
1021
|
+
graph = await resolved({ configurable: kwargs?.config?.configurable });
|
|
1022
|
+
} else {
|
|
1023
|
+
graph = resolved;
|
|
1024
|
+
}
|
|
1025
|
+
graph.checkpointer = deps.checkpointer;
|
|
1026
|
+
const states = [];
|
|
1027
|
+
const limit = options?.limit;
|
|
1028
|
+
for await (const snapshot of graph.getStateHistory({
|
|
1029
|
+
configurable: { thread_id: threadId }
|
|
1030
|
+
})) {
|
|
1031
|
+
states.push(snapshotToThreadState(snapshot));
|
|
1032
|
+
if (limit !== void 0 && states.length >= limit) break;
|
|
1033
|
+
}
|
|
1034
|
+
return states;
|
|
1035
|
+
};
|
|
1036
|
+
return {
|
|
1037
|
+
create: (input) => deps.store.threads.create(input),
|
|
1038
|
+
get: requireThread,
|
|
1039
|
+
list: () => deps.store.threads.list(),
|
|
1040
|
+
async patch(threadId, patch) {
|
|
1041
|
+
await requireThread(threadId);
|
|
1042
|
+
return deps.store.threads.update(threadId, { metadata: patch.metadata });
|
|
1043
|
+
},
|
|
1044
|
+
async delete(threadId) {
|
|
1045
|
+
await requireThread(threadId);
|
|
1046
|
+
const runs = await deps.store.runs.listByThread(threadId);
|
|
1047
|
+
for (const run of runs) {
|
|
1048
|
+
if (!isTerminalRunStatus4(run.status)) {
|
|
1049
|
+
control.abort(run.run_id, "cancel");
|
|
1050
|
+
await deps.bus.close(run.run_id);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
await deps.store.threads.delete(threadId);
|
|
1054
|
+
},
|
|
1055
|
+
history: readHistory,
|
|
1056
|
+
async getState(threadId) {
|
|
1057
|
+
const [current] = await readHistory(threadId, { limit: 1 });
|
|
1058
|
+
return current ?? emptyThreadState(threadId);
|
|
1059
|
+
}
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// src/threads/thread-stream-service.ts
|
|
1064
|
+
import {
|
|
1065
|
+
isTerminalRunStatus as isTerminalRunStatus5,
|
|
1066
|
+
SkeinHttpError as SkeinHttpError8
|
|
1067
|
+
} from "@skein-js/core";
|
|
1068
|
+
function createThreadStreamService(ctx, runs) {
|
|
1069
|
+
const { deps } = ctx;
|
|
1070
|
+
const requireThread = async (threadId) => {
|
|
1071
|
+
const thread = await deps.store.threads.get(threadId);
|
|
1072
|
+
if (!thread) throw SkeinHttpError8.notFound(`Thread "${threadId}" not found.`);
|
|
1073
|
+
return thread;
|
|
1074
|
+
};
|
|
1075
|
+
const latestRunAssistant = async (threadId) => {
|
|
1076
|
+
const threadRuns = await deps.store.runs.listByThread(threadId);
|
|
1077
|
+
const latest = [...threadRuns].sort((a, b) => b.created_at.localeCompare(a.created_at))[0];
|
|
1078
|
+
return latest?.assistant_id;
|
|
1079
|
+
};
|
|
1080
|
+
return {
|
|
1081
|
+
async stream(threadId, input) {
|
|
1082
|
+
await requireThread(threadId);
|
|
1083
|
+
return runs.createStream({ ...input, thread_id: threadId });
|
|
1084
|
+
},
|
|
1085
|
+
async joinStream(threadId, afterSeq) {
|
|
1086
|
+
await requireThread(threadId);
|
|
1087
|
+
const threadRuns = await deps.store.runs.listByThread(threadId);
|
|
1088
|
+
const sorted = [...threadRuns].sort((a, b) => b.created_at.localeCompare(a.created_at));
|
|
1089
|
+
const target = sorted.find((run) => !isTerminalRunStatus5(run.status)) ?? sorted[0];
|
|
1090
|
+
if (!target) throw SkeinHttpError8.notFound(`Thread "${threadId}" has no runs to stream.`);
|
|
1091
|
+
return { runId: target.run_id, frames: await runs.join(target.run_id, afterSeq) };
|
|
1092
|
+
},
|
|
1093
|
+
async command(threadId, input) {
|
|
1094
|
+
const thread = await requireThread(threadId);
|
|
1095
|
+
if (thread.status !== "interrupted") {
|
|
1096
|
+
throw SkeinHttpError8.conflict(`Thread "${threadId}" is not interrupted.`, {
|
|
1097
|
+
code: "thread_not_interrupted"
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
const command = input.command ?? (input.resume !== void 0 ? { resume: input.resume } : void 0);
|
|
1101
|
+
if (!command) {
|
|
1102
|
+
throw SkeinHttpError8.badRequest("A command must provide `command` or `resume`.");
|
|
1103
|
+
}
|
|
1104
|
+
const assistantId = input.assistant_id ?? await latestRunAssistant(threadId);
|
|
1105
|
+
if (!assistantId) {
|
|
1106
|
+
throw SkeinHttpError8.badRequest(
|
|
1107
|
+
"Cannot resume: no assistant_id and no prior run on the thread."
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
const runInput = {
|
|
1111
|
+
thread_id: threadId,
|
|
1112
|
+
assistant_id: assistantId,
|
|
1113
|
+
command,
|
|
1114
|
+
stream_mode: input.stream_mode,
|
|
1115
|
+
config: input.config,
|
|
1116
|
+
context: input.context,
|
|
1117
|
+
metadata: input.metadata
|
|
1118
|
+
};
|
|
1119
|
+
return runs.createStream(runInput);
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// src/service.ts
|
|
1125
|
+
function buildProtocolService(ctx) {
|
|
1126
|
+
const runs = createRunService(ctx);
|
|
1127
|
+
return {
|
|
1128
|
+
assistants: createAssistantService(ctx.deps),
|
|
1129
|
+
threads: createThreadService(ctx),
|
|
1130
|
+
threadStream: createThreadStreamService(ctx, runs),
|
|
1131
|
+
runs,
|
|
1132
|
+
store: createStoreService(ctx.deps)
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
function createProtocolService(deps) {
|
|
1136
|
+
return buildProtocolService(createContext(deps));
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// src/auth/auth-scoped-store.ts
|
|
1140
|
+
import {
|
|
1141
|
+
SkeinHttpError as SkeinHttpError9
|
|
1142
|
+
} from "@skein-js/core";
|
|
1143
|
+
function stampFromFilters(metadata, filters) {
|
|
1144
|
+
const stamped = { ...metadata ?? {} };
|
|
1145
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
1146
|
+
if (typeof value === "string") {
|
|
1147
|
+
stamped[key] = value;
|
|
1148
|
+
} else if (typeof value.$eq === "string") {
|
|
1149
|
+
stamped[key] = value.$eq;
|
|
1150
|
+
} else if (value.$contains !== void 0) {
|
|
1151
|
+
const required = Array.isArray(value.$contains) ? value.$contains : [value.$contains];
|
|
1152
|
+
const current = Array.isArray(stamped[key]) ? stamped[key] : [];
|
|
1153
|
+
stamped[key] = [.../* @__PURE__ */ new Set([...current, ...required])];
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
return stamped;
|
|
1157
|
+
}
|
|
1158
|
+
function createAuthScopedStore(inner, engine, filters, resource) {
|
|
1159
|
+
if (!filters) return inner;
|
|
1160
|
+
const matches = (metadata) => engine.matchesFilters(metadata ?? void 0, filters);
|
|
1161
|
+
const stamp = (metadata) => stampFromFilters(metadata ?? void 0, filters);
|
|
1162
|
+
if (resource === "threads") {
|
|
1163
|
+
const threads = {
|
|
1164
|
+
list: async () => (await inner.threads.list()).filter((thread) => matches(thread.metadata)),
|
|
1165
|
+
get: async (threadId) => {
|
|
1166
|
+
const thread = await inner.threads.get(threadId);
|
|
1167
|
+
return thread && matches(thread.metadata) ? thread : null;
|
|
1168
|
+
},
|
|
1169
|
+
create: async (input) => {
|
|
1170
|
+
if (input?.thread_id !== void 0) {
|
|
1171
|
+
const existing = await inner.threads.get(input.thread_id);
|
|
1172
|
+
if (existing && !matches(existing.metadata)) {
|
|
1173
|
+
throw SkeinHttpError9.notFound(`Thread "${input.thread_id}" not found.`);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
return inner.threads.create({ ...input, metadata: stamp(input?.metadata) });
|
|
1177
|
+
},
|
|
1178
|
+
update: async (threadId, patch) => {
|
|
1179
|
+
const thread = await inner.threads.get(threadId);
|
|
1180
|
+
if (!thread || !matches(thread.metadata)) {
|
|
1181
|
+
throw SkeinHttpError9.notFound(`Thread "${threadId}" not found.`);
|
|
1182
|
+
}
|
|
1183
|
+
const next = patch.metadata !== void 0 ? { ...patch, metadata: stamp(patch.metadata) } : patch;
|
|
1184
|
+
return inner.threads.update(threadId, next);
|
|
1185
|
+
},
|
|
1186
|
+
delete: async (threadId) => {
|
|
1187
|
+
const thread = await inner.threads.get(threadId);
|
|
1188
|
+
if (!thread || !matches(thread.metadata)) {
|
|
1189
|
+
throw SkeinHttpError9.notFound(`Thread "${threadId}" not found.`);
|
|
1190
|
+
}
|
|
1191
|
+
return inner.threads.delete(threadId);
|
|
1192
|
+
}
|
|
1193
|
+
};
|
|
1194
|
+
const runs = {
|
|
1195
|
+
...inner.runs,
|
|
1196
|
+
get: async (runId) => {
|
|
1197
|
+
const run = await inner.runs.get(runId);
|
|
1198
|
+
return run && matches(run.metadata) ? run : null;
|
|
1199
|
+
},
|
|
1200
|
+
listByThread: async (threadId) => (await inner.runs.listByThread(threadId)).filter((run) => matches(run.metadata)),
|
|
1201
|
+
create: (input) => inner.runs.create({ ...input, metadata: stamp(input.metadata) }),
|
|
1202
|
+
setStatus: async (runId, status) => {
|
|
1203
|
+
const run = await inner.runs.get(runId);
|
|
1204
|
+
if (!run || !matches(run.metadata)) {
|
|
1205
|
+
throw SkeinHttpError9.notFound(`Run "${runId}" not found.`);
|
|
1206
|
+
}
|
|
1207
|
+
return inner.runs.setStatus(runId, status);
|
|
1208
|
+
},
|
|
1209
|
+
delete: async (runId) => {
|
|
1210
|
+
const run = await inner.runs.get(runId);
|
|
1211
|
+
if (!run || !matches(run.metadata)) {
|
|
1212
|
+
throw SkeinHttpError9.notFound(`Run "${runId}" not found.`);
|
|
1213
|
+
}
|
|
1214
|
+
return inner.runs.delete(runId);
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1217
|
+
return { ...inner, threads, runs };
|
|
1218
|
+
}
|
|
1219
|
+
return inner;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// src/auth/route-authz.ts
|
|
1223
|
+
var ROUTE_AUTHZ = {
|
|
1224
|
+
// assistants
|
|
1225
|
+
getAssistant: { resource: "assistants", action: "read" },
|
|
1226
|
+
getAssistantSchemas: { resource: "assistants", action: "read" },
|
|
1227
|
+
searchAssistants: { resource: "assistants", action: "search" },
|
|
1228
|
+
// threads
|
|
1229
|
+
createThread: { resource: "threads", action: "create" },
|
|
1230
|
+
getThread: { resource: "threads", action: "read" },
|
|
1231
|
+
getThreadState: { resource: "threads", action: "read" },
|
|
1232
|
+
getThreadHistory: { resource: "threads", action: "read" },
|
|
1233
|
+
listThreads: { resource: "threads", action: "search" },
|
|
1234
|
+
patchThread: { resource: "threads", action: "update" },
|
|
1235
|
+
deleteThread: { resource: "threads", action: "delete" },
|
|
1236
|
+
// runs (authorized through the owning thread)
|
|
1237
|
+
createWaitRun: { resource: "threads", action: "create_run" },
|
|
1238
|
+
createStreamRun: { resource: "threads", action: "create_run" },
|
|
1239
|
+
createBackgroundRun: { resource: "threads", action: "create_run" },
|
|
1240
|
+
getRun: { resource: "threads", action: "read" },
|
|
1241
|
+
listThreadRuns: { resource: "threads", action: "read" },
|
|
1242
|
+
joinRunStream: { resource: "threads", action: "read" },
|
|
1243
|
+
cancelRun: { resource: "threads", action: "update" },
|
|
1244
|
+
deleteRun: { resource: "threads", action: "delete" },
|
|
1245
|
+
// thread streaming / commands
|
|
1246
|
+
postThreadStream: { resource: "threads", action: "create_run" },
|
|
1247
|
+
getThreadStream: { resource: "threads", action: "read" },
|
|
1248
|
+
postThreadCommands: { resource: "threads", action: "create_run" },
|
|
1249
|
+
// store
|
|
1250
|
+
putStoreItem: { resource: "store", action: "put" },
|
|
1251
|
+
getStoreItem: { resource: "store", action: "get" },
|
|
1252
|
+
deleteStoreItem: { resource: "store", action: "delete" },
|
|
1253
|
+
searchStoreItems: { resource: "store", action: "search" },
|
|
1254
|
+
listStoreNamespaces: { resource: "store", action: "list_namespaces" }
|
|
1255
|
+
};
|
|
1256
|
+
function authValue(req) {
|
|
1257
|
+
const body = typeof req.body === "object" && req.body !== null ? req.body : {};
|
|
1258
|
+
return { ...req.query, ...req.params, ...body };
|
|
1259
|
+
}
|
|
1260
|
+
var FRAMING_HEADERS = /* @__PURE__ */ new Set(["content-length", "transfer-encoding", "connection"]);
|
|
1261
|
+
function synthesizeRequest(req) {
|
|
1262
|
+
const headers = new Headers();
|
|
1263
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
1264
|
+
if (value !== void 0 && !FRAMING_HEADERS.has(name.toLowerCase())) headers.set(name, value);
|
|
1265
|
+
}
|
|
1266
|
+
const method = req.method.toUpperCase();
|
|
1267
|
+
const carriesBody = method !== "GET" && method !== "HEAD" && req.body !== void 0;
|
|
1268
|
+
return new Request(req.url, {
|
|
1269
|
+
method,
|
|
1270
|
+
headers,
|
|
1271
|
+
body: carriesBody ? JSON.stringify(req.body) : void 0
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// src/auth/authorizing-handlers.ts
|
|
1276
|
+
var STUDIO_USER = {
|
|
1277
|
+
identity: "langgraph-studio-user",
|
|
1278
|
+
display_name: "langgraph-studio-user",
|
|
1279
|
+
is_authenticated: true,
|
|
1280
|
+
permissions: []
|
|
1281
|
+
};
|
|
1282
|
+
function createAuthorizingHandlers(context, engine) {
|
|
1283
|
+
const baseHandlers = createProtocolHandlers(buildProtocolService(context));
|
|
1284
|
+
const names = Object.keys(baseHandlers);
|
|
1285
|
+
const resolveAuthContext = async (req) => {
|
|
1286
|
+
if (!engine.studioAuthDisabled && req.headers["x-auth-scheme"] === "langsmith") {
|
|
1287
|
+
return { user: STUDIO_USER, scopes: [] };
|
|
1288
|
+
}
|
|
1289
|
+
return engine.authenticate(synthesizeRequest(req));
|
|
1290
|
+
};
|
|
1291
|
+
const wrapped = {};
|
|
1292
|
+
for (const name of names) {
|
|
1293
|
+
const route = ROUTE_AUTHZ[name];
|
|
1294
|
+
wrapped[name] = async (req) => {
|
|
1295
|
+
const authContext = await resolveAuthContext(req);
|
|
1296
|
+
const { filters } = await engine.authorize({
|
|
1297
|
+
resource: route.resource,
|
|
1298
|
+
action: route.action,
|
|
1299
|
+
value: authValue(req),
|
|
1300
|
+
context: authContext
|
|
1301
|
+
});
|
|
1302
|
+
if (!filters) return baseHandlers[name](req);
|
|
1303
|
+
const scopedStore = createAuthScopedStore(context.deps.store, engine, filters, route.resource);
|
|
1304
|
+
const scopedContext = {
|
|
1305
|
+
...context,
|
|
1306
|
+
deps: { ...context.deps, store: scopedStore }
|
|
1307
|
+
};
|
|
1308
|
+
return createProtocolHandlers(buildProtocolService(scopedContext))[name](req);
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
return wrapped;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// src/runs/run-worker.ts
|
|
1315
|
+
import { isTerminalRunStatus as isTerminalRunStatus6 } from "@skein-js/core";
|
|
1316
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1317
|
+
function createRunWorker(ctx, options = {}) {
|
|
1318
|
+
const { deps, control } = ctx;
|
|
1319
|
+
const maxConcurrency = options.maxConcurrency ?? 1;
|
|
1320
|
+
const shutdownGraceMs = options.shutdownGraceMs ?? 5e3;
|
|
1321
|
+
const inFlight = /* @__PURE__ */ new Set();
|
|
1322
|
+
const process = async (queued) => {
|
|
1323
|
+
const run = await deps.store.runs.get(queued.run_id);
|
|
1324
|
+
if (!run || isTerminalRunStatus6(run.status)) return;
|
|
1325
|
+
const kwargs = await deps.store.runs.getKwargs(queued.run_id) ?? {};
|
|
1326
|
+
const runControl = control.register(queued.run_id);
|
|
1327
|
+
inFlight.add(queued.run_id);
|
|
1328
|
+
try {
|
|
1329
|
+
await executeRun(deps, { run, kwargs, control: runControl });
|
|
1330
|
+
} finally {
|
|
1331
|
+
control.clear(queued.run_id);
|
|
1332
|
+
inFlight.delete(queued.run_id);
|
|
1333
|
+
}
|
|
1334
|
+
};
|
|
1335
|
+
let consumer;
|
|
1336
|
+
return {
|
|
1337
|
+
start() {
|
|
1338
|
+
if (consumer) return;
|
|
1339
|
+
consumer = deps.queue.consume(process, { concurrency: maxConcurrency });
|
|
1340
|
+
},
|
|
1341
|
+
async stop() {
|
|
1342
|
+
if (!consumer) return;
|
|
1343
|
+
const active = consumer;
|
|
1344
|
+
consumer = void 0;
|
|
1345
|
+
let drained = false;
|
|
1346
|
+
const graceful = active.close().then(() => {
|
|
1347
|
+
drained = true;
|
|
1348
|
+
});
|
|
1349
|
+
await Promise.race([graceful, sleep(shutdownGraceMs)]);
|
|
1350
|
+
if (!drained) {
|
|
1351
|
+
for (const runId of inFlight) control.abort(runId, "cancel");
|
|
1352
|
+
await graceful;
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// src/runtime.ts
|
|
1359
|
+
function createProtocolRuntime(deps, options = {}) {
|
|
1360
|
+
const context = createContext(deps);
|
|
1361
|
+
const service = buildProtocolService(context);
|
|
1362
|
+
const handlers = deps.auth ? createAuthorizingHandlers(context, deps.auth) : createProtocolHandlers(service);
|
|
1363
|
+
return {
|
|
1364
|
+
service,
|
|
1365
|
+
handlers,
|
|
1366
|
+
worker: createRunWorker(context, options.worker)
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
export {
|
|
1370
|
+
SSE_HEADERS,
|
|
1371
|
+
SkeinBaseStore,
|
|
1372
|
+
buildProtocolService,
|
|
1373
|
+
createContext,
|
|
1374
|
+
createProtocolHandlers,
|
|
1375
|
+
createProtocolRuntime,
|
|
1376
|
+
createProtocolService,
|
|
1377
|
+
createRunWorker,
|
|
1378
|
+
encodeFrame,
|
|
1379
|
+
encodeTerminal,
|
|
1380
|
+
parseAfterSeq,
|
|
1381
|
+
toSseEvents
|
|
1382
|
+
};
|