@langchain/langgraph-api 1.1.16 → 1.2.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.
Files changed (52) hide show
  1. package/dist/api/protocol.d.mts +7 -0
  2. package/dist/api/protocol.mjs +157 -0
  3. package/dist/api/runs.mjs +15 -4
  4. package/dist/command.mjs +1 -1
  5. package/dist/experimental/embed/constants.d.mts +3 -0
  6. package/dist/experimental/embed/constants.mjs +11 -0
  7. package/dist/experimental/embed/protocol.d.mts +9 -0
  8. package/dist/experimental/embed/protocol.mjs +453 -0
  9. package/dist/experimental/embed/runs.d.mts +8 -0
  10. package/dist/experimental/embed/runs.mjs +202 -0
  11. package/dist/experimental/embed/threads.d.mts +8 -0
  12. package/dist/experimental/embed/threads.mjs +151 -0
  13. package/dist/experimental/embed/types.d.mts +77 -0
  14. package/dist/experimental/embed/types.mjs +1 -0
  15. package/dist/experimental/embed/utils.d.mts +29 -0
  16. package/dist/experimental/embed/utils.mjs +62 -0
  17. package/dist/experimental/embed.d.mts +11 -31
  18. package/dist/experimental/embed.mjs +19 -399
  19. package/dist/graph/load.d.mts +2 -2
  20. package/dist/graph/load.utils.mjs +13 -3
  21. package/dist/protocol/constants.d.mts +7 -0
  22. package/dist/protocol/constants.mjs +7 -0
  23. package/dist/protocol/service.d.mts +101 -0
  24. package/dist/protocol/service.mjs +568 -0
  25. package/dist/protocol/session/event-normalizers.d.mts +52 -0
  26. package/dist/protocol/session/event-normalizers.mjs +162 -0
  27. package/dist/protocol/session/index.d.mts +261 -0
  28. package/dist/protocol/session/index.mjs +826 -0
  29. package/dist/protocol/session/internal-types.d.mts +67 -0
  30. package/dist/protocol/session/internal-types.mjs +28 -0
  31. package/dist/protocol/session/metadata.d.mts +24 -0
  32. package/dist/protocol/session/metadata.mjs +95 -0
  33. package/dist/protocol/session/namespace.d.mts +47 -0
  34. package/dist/protocol/session/namespace.mjs +62 -0
  35. package/dist/protocol/session/state-normalizers.d.mts +57 -0
  36. package/dist/protocol/session/state-normalizers.mjs +430 -0
  37. package/dist/protocol/session/tool-calls.d.mts +27 -0
  38. package/dist/protocol/session/tool-calls.mjs +59 -0
  39. package/dist/protocol/types.d.mts +121 -0
  40. package/dist/protocol/types.mjs +1 -0
  41. package/dist/queue.mjs +8 -2
  42. package/dist/schemas.d.mts +58 -58
  43. package/dist/semver/index.mjs +19 -1
  44. package/dist/server.mjs +22 -3
  45. package/dist/state.mjs +1 -1
  46. package/dist/storage/ops.mjs +17 -5
  47. package/dist/storage/persist.mjs +1 -1
  48. package/dist/storage/types.d.mts +10 -1
  49. package/dist/stream.d.mts +25 -4
  50. package/dist/stream.mjs +203 -11
  51. package/dist/utils/serde.mjs +1 -1
  52. package/package.json +16 -14
@@ -0,0 +1,453 @@
1
+ import { zValidator } from "@hono/zod-validator";
2
+ import { streamSSE } from "hono/streaming";
3
+ import { v7 as uuidv7 } from "uuid";
4
+ import { z } from "zod/v3";
5
+ import { streamState } from "../../stream.mjs";
6
+ import { serialiseAsDict } from "../../utils/serde.mjs";
7
+ import { jsonExtra } from "../../utils/hono.mjs";
8
+ import { RunProtocolSession } from "../../protocol/session/index.mjs";
9
+ import { PROTOCOL_STREAM_RUN_KEY } from "../../protocol/constants.mjs";
10
+ import { matchesSinkFilter } from "../../protocol/service.mjs";
11
+ import { ProtocolCommandSchema, ThreadIdSchema, isRecord, createStubRun, } from "./utils.mjs";
12
+ import { DEFAULT_PROTOCOL_STREAM_MODES } from "./constants.mjs";
13
+ const EventsFilterSchema = z
14
+ .object({
15
+ channels: z.array(z.string()),
16
+ namespaces: z.array(z.array(z.string())).optional(),
17
+ depth: z.number().int().nonnegative().optional(),
18
+ since: z.number().int().nonnegative().optional(),
19
+ })
20
+ .strict();
21
+ /**
22
+ * Normalize browser/node websocket payloads into UTF-8 text so the
23
+ * protocol layer only needs to handle JSON strings.
24
+ */
25
+ const parseSocketPayload = async (event) => {
26
+ if (typeof event.data === "string")
27
+ return event.data;
28
+ if (event.data instanceof ArrayBuffer) {
29
+ return new TextDecoder().decode(new Uint8Array(event.data));
30
+ }
31
+ if (event.data instanceof Blob) {
32
+ const buffer = await event.data.arrayBuffer();
33
+ return new TextDecoder().decode(new Uint8Array(buffer));
34
+ }
35
+ return String(event.data);
36
+ };
37
+ /**
38
+ * Register thread-centric v2 protocol routes on an embed server Hono app.
39
+ *
40
+ * @experimental Does not follow semver.
41
+ */
42
+ export function registerProtocolRoutes(api, context, upgradeWebSocket) {
43
+ const threads = new Map();
44
+ function ensureThread(threadId) {
45
+ let thread = threads.get(threadId);
46
+ if (thread == null) {
47
+ thread = {
48
+ threadId,
49
+ seq: 0,
50
+ eventSinks: new Map(),
51
+ queuedEvents: [],
52
+ };
53
+ threads.set(threadId, thread);
54
+ }
55
+ return thread;
56
+ }
57
+ async function* trackRunStatus(source, run) {
58
+ try {
59
+ yield* source;
60
+ run.status = "success";
61
+ }
62
+ catch (error) {
63
+ run.status = "error";
64
+ throw error;
65
+ }
66
+ }
67
+ function attachRunSession(thread, run) {
68
+ const rawSource = streamState(run, {
69
+ attempt: 1,
70
+ getGraph: context.getGraph,
71
+ signal: undefined,
72
+ });
73
+ const source = trackRunStatus(rawSource, run);
74
+ const protocolSession = new RunProtocolSession({
75
+ runId: run.run_id,
76
+ threadId: thread.threadId,
77
+ initialRun: run,
78
+ getRun: async () => thread.currentRun ?? null,
79
+ getThreadState: async () => {
80
+ const persisted = await context.threads.get(thread.threadId);
81
+ const graphId = persisted?.metadata?.graph_id;
82
+ if (!graphId)
83
+ return null;
84
+ const graph = await context.getGraph(graphId);
85
+ const snapshot = await graph.getState({ configurable: { thread_id: thread.threadId } }, { subgraphs: true });
86
+ return {
87
+ tasks: snapshot.tasks.map((t) => ({
88
+ interrupts: t.interrupts,
89
+ })),
90
+ };
91
+ },
92
+ source,
93
+ startSeq: thread.seq,
94
+ passthrough: true,
95
+ send: async (payload) => {
96
+ const parsed = JSON.parse(payload);
97
+ thread.seq = Math.max(thread.seq, parsed.seq ?? thread.seq);
98
+ // Always buffer events so late-attaching sinks can replay
99
+ // matching history. Sinks with `pendingReplay` are skipped
100
+ // here; their replay loop delivers this event in buffer order.
101
+ thread.queuedEvents.push(parsed);
102
+ for (const sink of thread.eventSinks.values()) {
103
+ if (sink.pendingReplay)
104
+ continue;
105
+ if (sink.unfiltered || matchesSinkFilter(sink.filter, parsed)) {
106
+ await sink.send(parsed);
107
+ }
108
+ }
109
+ },
110
+ });
111
+ thread.runSession = protocolSession;
112
+ thread.currentRun = run;
113
+ return protocolSession;
114
+ }
115
+ async function handleRunStart(thread, command) {
116
+ const params = isRecord(command.params)
117
+ ? command.params
118
+ : {};
119
+ const assistantId = typeof params.assistant_id === "string" ? params.assistant_id : undefined;
120
+ if (!assistantId) {
121
+ return jsonResponse({
122
+ type: "error",
123
+ id: command.id,
124
+ error: "invalid_argument",
125
+ message: "run.start requires an assistant_id.",
126
+ });
127
+ }
128
+ if (thread.assistantId != null && thread.assistantId !== assistantId) {
129
+ return jsonResponse({
130
+ type: "error",
131
+ id: command.id,
132
+ error: "invalid_argument",
133
+ message: `Thread ${thread.threadId} is bound to assistant ${thread.assistantId}; cannot run ${assistantId}.`,
134
+ });
135
+ }
136
+ thread.assistantId = assistantId;
137
+ const runMetadata = isRecord(params.metadata)
138
+ ? params.metadata
139
+ : {};
140
+ // Lazily create the persisted thread on first use.
141
+ let persisted = null;
142
+ try {
143
+ persisted = await context.threads.get(thread.threadId);
144
+ }
145
+ catch {
146
+ persisted = null;
147
+ }
148
+ if (persisted == null) {
149
+ await context.threads.set(thread.threadId, {
150
+ kind: "put",
151
+ metadata: {
152
+ ...runMetadata,
153
+ graph_id: assistantId,
154
+ assistant_id: assistantId,
155
+ },
156
+ });
157
+ }
158
+ else {
159
+ await context.threads.set(thread.threadId, {
160
+ kind: "patch",
161
+ metadata: {
162
+ ...runMetadata,
163
+ graph_id: assistantId,
164
+ assistant_id: assistantId,
165
+ },
166
+ });
167
+ }
168
+ // Promote SDK-side `forkFrom: { checkpointId }` into
169
+ // `configurable.checkpoint_id` so the engine replays from the
170
+ // requested fork target. This mirrors the promotion performed by
171
+ // `ProtocolService.createOrResumeRun` in the non-embed path and
172
+ // closes the "client sends forkFrom, server drops it" gap.
173
+ const forkCheckpointId = (() => {
174
+ if (!isRecord(params.forkFrom))
175
+ return undefined;
176
+ const id = params.forkFrom.checkpointId;
177
+ return typeof id === "string" && id.length > 0 ? id : undefined;
178
+ })();
179
+ const run = createStubRun(thread.threadId, {
180
+ assistant_id: assistantId,
181
+ on_disconnect: "cancel",
182
+ input: params.input ?? null,
183
+ config: {
184
+ configurable: {
185
+ ...(isRecord(params.config) && isRecord(params.config.configurable)
186
+ ? params.config.configurable
187
+ : {}),
188
+ },
189
+ },
190
+ // `createStubRun` promotes the top-level `checkpoint_id` into
191
+ // `config.configurable.checkpoint_id` after merging (and, in
192
+ // fact, *replaces* any inline configurable the caller passed),
193
+ // so this is the only reliable way to reach the engine with a
194
+ // fork target.
195
+ ...(forkCheckpointId != null ? { checkpoint_id: forkCheckpointId } : {}),
196
+ metadata: Object.keys(runMetadata).length > 0 ? runMetadata : undefined,
197
+ stream_mode: DEFAULT_PROTOCOL_STREAM_MODES,
198
+ stream_subgraphs: true,
199
+ });
200
+ run.kwargs[PROTOCOL_STREAM_RUN_KEY] = true;
201
+ const protocolSession = attachRunSession(thread, run);
202
+ await protocolSession.start();
203
+ return jsonResponse({
204
+ type: "success",
205
+ id: command.id,
206
+ result: { run_id: run.run_id },
207
+ meta: {
208
+ thread_id: thread.threadId,
209
+ applied_through_seq: thread.seq,
210
+ },
211
+ });
212
+ }
213
+ async function handleInputRespond(thread, command) {
214
+ const params = isRecord(command.params)
215
+ ? command.params
216
+ : {};
217
+ const interruptId = params.interrupt_id;
218
+ if (typeof interruptId !== "string") {
219
+ return jsonResponse({
220
+ type: "error",
221
+ id: command.id,
222
+ error: "invalid_argument",
223
+ message: "input.respond requires an interrupt_id.",
224
+ });
225
+ }
226
+ const assistantId = thread.assistantId;
227
+ if (assistantId == null) {
228
+ return jsonResponse({
229
+ type: "error",
230
+ id: command.id,
231
+ error: "no_such_run",
232
+ message: "Thread has no active assistant; call run.start first.",
233
+ });
234
+ }
235
+ const run = createStubRun(thread.threadId, {
236
+ assistant_id: assistantId,
237
+ on_disconnect: "cancel",
238
+ input: null,
239
+ command: { resume: { [interruptId]: params.response } },
240
+ stream_mode: DEFAULT_PROTOCOL_STREAM_MODES,
241
+ stream_subgraphs: true,
242
+ });
243
+ run.kwargs[PROTOCOL_STREAM_RUN_KEY] = true;
244
+ // Drop the previous run's buffered events so that a sink attaching
245
+ // *after* this resume does not replay stale terminal lifecycle
246
+ // events (e.g. `lifecycle.interrupted`) from the paused run. Any
247
+ // sink that was already receiving live events is unaffected.
248
+ thread.queuedEvents.length = 0;
249
+ const protocolSession = attachRunSession(thread, run);
250
+ await protocolSession.start();
251
+ return jsonResponse({
252
+ type: "success",
253
+ id: command.id,
254
+ result: {},
255
+ meta: {
256
+ thread_id: thread.threadId,
257
+ applied_through_seq: thread.seq,
258
+ },
259
+ });
260
+ }
261
+ async function handleThreadCommand(thread, command) {
262
+ if (command.method === "run.start") {
263
+ return await handleRunStart(thread, command);
264
+ }
265
+ if (command.method === "input.respond") {
266
+ return await handleInputRespond(thread, command);
267
+ }
268
+ // WebSocket transports send `subscription.subscribe`/`unsubscribe`
269
+ // over the same socket before any run is bound. The embed server
270
+ // delivers *all* events to the WS sink and relies on the SDK to
271
+ // filter client-side, so we can accept these commands without
272
+ // additional bookkeeping.
273
+ if (command.method === "subscription.subscribe") {
274
+ const subscriptionId = uuidv7();
275
+ return jsonResponse({
276
+ type: "success",
277
+ id: command.id,
278
+ result: { subscription_id: subscriptionId },
279
+ meta: {
280
+ thread_id: thread.threadId,
281
+ applied_through_seq: thread.seq,
282
+ },
283
+ });
284
+ }
285
+ if (command.method === "subscription.unsubscribe") {
286
+ return jsonResponse({
287
+ type: "success",
288
+ id: command.id,
289
+ result: {},
290
+ meta: {
291
+ thread_id: thread.threadId,
292
+ applied_through_seq: thread.seq,
293
+ },
294
+ });
295
+ }
296
+ if (thread.runSession == null) {
297
+ return jsonResponse({
298
+ type: "error",
299
+ id: command.id,
300
+ error: "no_such_run",
301
+ message: "No active run is bound to this thread.",
302
+ });
303
+ }
304
+ return jsonResponse(await thread.runSession.handleProtocolCommand(command, {
305
+ thread_id: thread.threadId,
306
+ applied_through_seq: thread.seq,
307
+ }));
308
+ }
309
+ function jsonResponse(body) {
310
+ return new Response(serialiseAsDict(body), {
311
+ headers: { "Content-Type": "application/json" },
312
+ });
313
+ }
314
+ api.post("/v2/threads/:thread_id/commands", zValidator("param", ThreadIdSchema), zValidator("json", ProtocolCommandSchema), async (c) => {
315
+ const { thread_id } = c.req.valid("param");
316
+ const thread = ensureThread(thread_id);
317
+ const command = c.req.valid("json");
318
+ return await handleThreadCommand(thread, command);
319
+ });
320
+ api.post("/v2/threads/:thread_id/stream", zValidator("param", ThreadIdSchema), zValidator("json", EventsFilterSchema), async (c) => {
321
+ const { thread_id } = c.req.valid("param");
322
+ const thread = ensureThread(thread_id);
323
+ const body = c.req.valid("json");
324
+ const sinkId = uuidv7();
325
+ const filter = {
326
+ channels: new Set(body.channels),
327
+ namespaces: body.namespaces,
328
+ depth: body.depth,
329
+ since: body.since,
330
+ };
331
+ return streamSSE(c, async (stream) => {
332
+ const delivered = new Set();
333
+ const writeSse = async (event) => {
334
+ if (event.event_id == null)
335
+ return;
336
+ if (delivered.has(event.event_id))
337
+ return;
338
+ delivered.add(event.event_id);
339
+ await stream.writeSSE({
340
+ id: event.event_id,
341
+ event: event.method,
342
+ data: serialiseAsDict(event),
343
+ });
344
+ };
345
+ // Register the sink as replaying so the live `send` path skips
346
+ // it while we drain buffered events in order. A cursor-based
347
+ // drain catches any events pushed during our awaits, then we
348
+ // unblock live delivery.
349
+ const sink = {
350
+ id: sinkId,
351
+ filter,
352
+ send: writeSse,
353
+ pendingReplay: true,
354
+ };
355
+ thread.eventSinks.set(sinkId, sink);
356
+ try {
357
+ let cursor = 0;
358
+ while (cursor < thread.queuedEvents.length) {
359
+ const event = thread.queuedEvents[cursor++];
360
+ if (matchesSinkFilter(filter, event)) {
361
+ await writeSse(event);
362
+ }
363
+ }
364
+ }
365
+ finally {
366
+ sink.pendingReplay = false;
367
+ }
368
+ stream.onAbort(() => {
369
+ thread.eventSinks.delete(sinkId);
370
+ });
371
+ await new Promise((resolve) => {
372
+ stream.onAbort(() => resolve());
373
+ });
374
+ });
375
+ });
376
+ if (upgradeWebSocket != null) {
377
+ api.get("/v2/threads/:thread_id/stream", zValidator("param", ThreadIdSchema), upgradeWebSocket((c) => {
378
+ const { thread_id } = c.req.valid("param");
379
+ const thread = ensureThread(thread_id);
380
+ const sinkId = uuidv7();
381
+ return {
382
+ async onOpen(_event, ws) {
383
+ // Unfiltered sink: forward every buffered + live event to the
384
+ // websocket in order, so the browser ThreadStream sees the
385
+ // complete event history regardless of subscription timing.
386
+ const writeWs = async (event) => {
387
+ ws.send(serialiseAsDict(event));
388
+ };
389
+ const sink = {
390
+ id: sinkId,
391
+ filter: {
392
+ channels: new Set(),
393
+ namespaces: undefined,
394
+ depth: undefined,
395
+ since: undefined,
396
+ },
397
+ send: writeWs,
398
+ pendingReplay: true,
399
+ unfiltered: true,
400
+ };
401
+ thread.eventSinks.set(sinkId, sink);
402
+ try {
403
+ let cursor = 0;
404
+ while (cursor < thread.queuedEvents.length) {
405
+ await writeWs(thread.queuedEvents[cursor++]);
406
+ }
407
+ }
408
+ finally {
409
+ sink.pendingReplay = false;
410
+ }
411
+ },
412
+ async onMessage(event, ws) {
413
+ let payload;
414
+ try {
415
+ payload = JSON.parse(await parseSocketPayload(event));
416
+ }
417
+ catch {
418
+ ws.send(serialiseAsDict({
419
+ type: "error",
420
+ id: null,
421
+ error: "invalid_argument",
422
+ message: "Protocol commands must be valid JSON.",
423
+ }));
424
+ return;
425
+ }
426
+ if (typeof payload !== "object" ||
427
+ payload == null ||
428
+ typeof payload.id !== "number" ||
429
+ typeof payload.method !== "string") {
430
+ ws.send(serialiseAsDict({
431
+ type: "error",
432
+ id: null,
433
+ error: "invalid_argument",
434
+ message: "Protocol commands must include an integer id and string method.",
435
+ }));
436
+ return;
437
+ }
438
+ const response = await handleThreadCommand(thread, payload);
439
+ const body = await response.text();
440
+ ws.send(body);
441
+ },
442
+ onClose() {
443
+ thread.eventSinks.delete(sinkId);
444
+ },
445
+ onError() {
446
+ thread.eventSinks.delete(sinkId);
447
+ },
448
+ };
449
+ }));
450
+ }
451
+ // jsonExtra is no longer needed but keep import indirection minimal.
452
+ void jsonExtra;
453
+ }
@@ -0,0 +1,8 @@
1
+ import type { Hono } from "hono";
2
+ import type { EmbedRouteContext } from "./types.mjs";
3
+ /**
4
+ * Register run creation and streaming routes on an embed server Hono app.
5
+ *
6
+ * @experimental Does not follow semver.
7
+ */
8
+ export declare function registerRunRoutes(api: Hono, context: EmbedRouteContext): void;
@@ -0,0 +1,202 @@
1
+ import { zValidator } from "@hono/zod-validator";
2
+ import { streamSSE } from "hono/streaming";
3
+ import { v7 as uuidv7 } from "uuid";
4
+ import { z } from "zod/v3";
5
+ import * as schemas from "../../schemas.mjs";
6
+ import { streamState } from "../../stream.mjs";
7
+ import { serialiseAsDict, serializeError } from "../../utils/serde.mjs";
8
+ import { getDisconnectAbortSignal, jsonExtra } from "../../utils/hono.mjs";
9
+ import { createStubRun } from "./utils.mjs";
10
+ /**
11
+ * Register run creation and streaming routes on an embed server Hono app.
12
+ *
13
+ * @experimental Does not follow semver.
14
+ */
15
+ export function registerRunRoutes(api, context) {
16
+ const threadRunState = new Map();
17
+ function getThreadState(threadId) {
18
+ let state = threadRunState.get(threadId);
19
+ if (!state) {
20
+ state = { activeRunId: null, pendingRuns: [] };
21
+ threadRunState.set(threadId, state);
22
+ }
23
+ return state;
24
+ }
25
+ async function waitForRunReady(threadId, runId, signal) {
26
+ const state = getThreadState(threadId);
27
+ const run = state.pendingRuns.find((r) => r.run_id === runId);
28
+ if (!run)
29
+ return null;
30
+ while (true) {
31
+ if (signal?.aborted) {
32
+ throw new DOMException("Aborted", "AbortError");
33
+ }
34
+ const isHead = state.pendingRuns[0]?.run_id === runId;
35
+ const noActive = !state.activeRunId;
36
+ if (isHead && noActive)
37
+ break;
38
+ await new Promise((r) => setTimeout(r, 50));
39
+ }
40
+ return run;
41
+ }
42
+ api.post("/threads/:thread_id/runs", zValidator("param", z.object({ thread_id: z.string().uuid() })), zValidator("json", schemas.RunCreate), async (c) => {
43
+ const { thread_id } = c.req.valid("param");
44
+ const payload = c.req.valid("json");
45
+ const thread = await context.threads.get(thread_id);
46
+ if (thread == null)
47
+ return c.json({ error: "Thread not found" }, 404);
48
+ const state = getThreadState(thread_id);
49
+ const multitaskStrategy = payload.multitask_strategy ?? "reject";
50
+ const shouldEnqueue = multitaskStrategy === "enqueue" && state.activeRunId != null;
51
+ const run = createStubRun(thread_id, payload, {
52
+ status: shouldEnqueue ? "pending" : "running",
53
+ multitask_strategy: multitaskStrategy,
54
+ });
55
+ state.pendingRuns.push(run);
56
+ c.header("Content-Location", `/threads/${thread_id}/runs/${run.run_id}`);
57
+ return jsonExtra(c, run);
58
+ });
59
+ api.get("/threads/:thread_id/runs/:run_id/stream", zValidator("param", z.object({
60
+ thread_id: z.string().uuid(),
61
+ run_id: z.string().uuid(),
62
+ })), async (c) => {
63
+ const { thread_id, run_id } = c.req.valid("param");
64
+ const thread = await context.threads.get(thread_id);
65
+ if (thread == null)
66
+ return c.json({ error: "Thread not found" }, 404);
67
+ return streamSSE(c, async (stream) => {
68
+ const signal = getDisconnectAbortSignal(c, stream);
69
+ const state = getThreadState(thread_id);
70
+ try {
71
+ const run = await waitForRunReady(thread_id, run_id, signal);
72
+ if (!run) {
73
+ await stream.writeSSE({
74
+ data: serialiseAsDict({ error: "Run not found" }),
75
+ event: "error",
76
+ });
77
+ return;
78
+ }
79
+ const idx = state.pendingRuns.findIndex((r) => r.run_id === run_id);
80
+ if (idx >= 0)
81
+ state.pendingRuns.splice(idx, 1);
82
+ state.activeRunId = run_id;
83
+ run.status = "running";
84
+ try {
85
+ for await (const { event, data } of streamState(run, {
86
+ attempt: 1,
87
+ getGraph: context.getGraph,
88
+ signal,
89
+ })) {
90
+ await stream.writeSSE({ data: serialiseAsDict(data), event });
91
+ }
92
+ }
93
+ catch (error) {
94
+ await stream.writeSSE({
95
+ data: serialiseAsDict(serializeError(error)),
96
+ event: "error",
97
+ });
98
+ }
99
+ finally {
100
+ if (state.activeRunId === run_id) {
101
+ state.activeRunId = null;
102
+ }
103
+ }
104
+ }
105
+ catch (err) {
106
+ if (err instanceof DOMException && err.name === "AbortError") {
107
+ return;
108
+ }
109
+ throw err;
110
+ }
111
+ });
112
+ });
113
+ api.post("/threads/:thread_id/runs/:run_id/cancel", zValidator("param", z.object({
114
+ thread_id: z.string().uuid(),
115
+ run_id: z.string().uuid(),
116
+ })), async (c) => {
117
+ const { thread_id, run_id } = c.req.valid("param");
118
+ const state = getThreadState(thread_id);
119
+ const idx = state.pendingRuns.findIndex((r) => r.run_id === run_id);
120
+ if (idx >= 0) {
121
+ state.pendingRuns.splice(idx, 1);
122
+ }
123
+ return new Response(null, { status: 204 });
124
+ });
125
+ api.post("/threads/:thread_id/runs/stream", zValidator("param", z.object({ thread_id: z.string().uuid() })), zValidator("json", schemas.RunCreate), async (c) => {
126
+ const { thread_id } = c.req.valid("param");
127
+ const payload = c.req.valid("json");
128
+ const thread = await context.threads.get(thread_id);
129
+ if (thread == null)
130
+ return c.json({ error: "Thread not found" }, 404);
131
+ const state = getThreadState(thread_id);
132
+ const run = createStubRun(thread_id, payload);
133
+ c.header("Content-Location", `/threads/${thread_id}/runs/${run.run_id}`);
134
+ return streamSSE(c, async (stream) => {
135
+ const signal = getDisconnectAbortSignal(c, stream);
136
+ state.activeRunId = run.run_id;
137
+ await context.threads.set(thread_id, {
138
+ kind: "patch",
139
+ metadata: {
140
+ graph_id: payload.assistant_id,
141
+ assistant_id: payload.assistant_id,
142
+ },
143
+ });
144
+ try {
145
+ for await (const { event, data } of streamState(run, {
146
+ attempt: 1,
147
+ getGraph: context.getGraph,
148
+ signal,
149
+ })) {
150
+ await stream.writeSSE({ data: serialiseAsDict(data), event });
151
+ }
152
+ }
153
+ catch (error) {
154
+ await stream.writeSSE({
155
+ data: serialiseAsDict(serializeError(error)),
156
+ event: "error",
157
+ });
158
+ }
159
+ finally {
160
+ if (state.activeRunId === run.run_id) {
161
+ state.activeRunId = null;
162
+ }
163
+ }
164
+ });
165
+ });
166
+ api.post("/runs/stream", zValidator("json", schemas.RunCreate), async (c) => {
167
+ const payload = c.req.valid("json");
168
+ const threadId = uuidv7();
169
+ const run = createStubRun(threadId, payload);
170
+ c.header("Content-Location", `/threads/${threadId}/runs/${run.run_id}`);
171
+ return streamSSE(c, async (stream) => {
172
+ const signal = getDisconnectAbortSignal(c, stream);
173
+ await context.threads.set(threadId, {
174
+ kind: "put",
175
+ metadata: {
176
+ graph_id: payload.assistant_id,
177
+ assistant_id: payload.assistant_id,
178
+ },
179
+ });
180
+ try {
181
+ try {
182
+ for await (const { event, data } of streamState(run, {
183
+ attempt: 1,
184
+ getGraph: context.getGraph,
185
+ signal,
186
+ })) {
187
+ await stream.writeSSE({ data: serialiseAsDict(data), event });
188
+ }
189
+ }
190
+ catch (error) {
191
+ await stream.writeSSE({
192
+ data: serialiseAsDict(serializeError(error)),
193
+ event: "error",
194
+ });
195
+ }
196
+ }
197
+ finally {
198
+ await context.threads.delete(threadId);
199
+ }
200
+ });
201
+ });
202
+ }
@@ -0,0 +1,8 @@
1
+ import type { Hono } from "hono";
2
+ import type { EmbedRouteContext } from "./types.mjs";
3
+ /**
4
+ * Register thread CRUD and state routes on an embed server Hono app.
5
+ *
6
+ * @experimental Does not follow semver.
7
+ */
8
+ export declare function registerThreadRoutes(api: Hono, context: EmbedRouteContext): void;