@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,568 @@
1
+ import { v7 as uuid7 } from "uuid";
2
+ import { getAssistantId } from "../graph/load.mjs";
3
+ import { isSupportedChannel, isRecord as isRecordInternal, } from "./session/internal-types.mjs";
4
+ import { PROTOCOL_STREAM_RUN_KEY } from "./constants.mjs";
5
+ import { RunProtocolSession } from "./session/index.mjs";
6
+ const DEFAULT_RUN_STREAM_MODES = [
7
+ "values",
8
+ "updates",
9
+ "messages",
10
+ "tools",
11
+ "custom",
12
+ "tasks",
13
+ ];
14
+ const isRecord = (value) => typeof value === "object" && value !== null;
15
+ const normalizeForkFrom = (value) => {
16
+ if (!isRecord(value))
17
+ return undefined;
18
+ const checkpointId = value.checkpointId;
19
+ if (typeof checkpointId !== "string" || checkpointId.length === 0) {
20
+ return undefined;
21
+ }
22
+ return { checkpointId };
23
+ };
24
+ const normalizeRunStart = (value) => {
25
+ if (isRecord(value)) {
26
+ return {
27
+ assistant_id: typeof value.assistant_id === "string" ? value.assistant_id : "",
28
+ input: value.input,
29
+ config: isRecord(value.config) ? value.config : undefined,
30
+ metadata: isRecord(value.metadata) ? value.metadata : undefined,
31
+ forkFrom: normalizeForkFrom(value.forkFrom),
32
+ };
33
+ }
34
+ return {
35
+ assistant_id: "",
36
+ input: undefined,
37
+ config: undefined,
38
+ metadata: undefined,
39
+ };
40
+ };
41
+ const normalizeKeys = (value) => {
42
+ if (!Array.isArray(value))
43
+ return undefined;
44
+ if (!value.every((entry) => typeof entry === "string"))
45
+ return undefined;
46
+ return value;
47
+ };
48
+ /**
49
+ * Thread-scoped connection registry and command dispatcher.
50
+ *
51
+ * In the thread-centric protocol, a `ThreadRecord` holds ephemeral
52
+ * connection state for an active client interacting with a thread. The
53
+ * thread itself is durable (lives in the checkpoint store); records are
54
+ * created lazily on first interaction and dropped when all connections
55
+ * close.
56
+ */
57
+ export class ProtocolService {
58
+ bindings;
59
+ threads = new Map();
60
+ constructor(bindings) {
61
+ this.bindings = bindings;
62
+ }
63
+ getThread(threadId) {
64
+ return this.threads.get(threadId);
65
+ }
66
+ /**
67
+ * Get or create the in-memory record for a thread. Records hold
68
+ * ephemeral connection state (event sinks, current run session) and
69
+ * are created on first use for any thread the client targets.
70
+ */
71
+ ensureThread(options) {
72
+ let record = this.threads.get(options.threadId);
73
+ if (record == null) {
74
+ record = {
75
+ threadId: options.threadId,
76
+ transport: options.transport,
77
+ auth: options.auth,
78
+ seq: 0,
79
+ session: undefined,
80
+ currentRunId: undefined,
81
+ sendEvent: options.sendEvent,
82
+ eventSinks: new Map(),
83
+ queuedEvents: [],
84
+ activeSubscriptions: [],
85
+ pendingSubscribes: [],
86
+ };
87
+ this.threads.set(options.threadId, record);
88
+ }
89
+ else if (options.sendEvent != null) {
90
+ record.sendEvent = options.sendEvent;
91
+ }
92
+ return record;
93
+ }
94
+ /**
95
+ * Attach a live transport consumer (WebSocket) and flush any buffered
96
+ * events.
97
+ */
98
+ async attachEventSink(threadId, sendEvent) {
99
+ const record = this.requireThread(threadId);
100
+ record.sendEvent = sendEvent;
101
+ for (const event of record.queuedEvents.splice(0)) {
102
+ await sendEvent(event);
103
+ }
104
+ return record;
105
+ }
106
+ /**
107
+ * Attach a filtered SSE event sink and replay buffered events that
108
+ * match the filter.
109
+ *
110
+ * The sink is flagged `pendingReplay` while draining so that the live
111
+ * `send` path skips it — preventing live events from interleaving with
112
+ * the replay loop's awaits and producing out-of-order delivery.
113
+ */
114
+ async attachFilteredEventSink(threadId, sink) {
115
+ const record = this.requireThread(threadId);
116
+ sink.pendingReplay = true;
117
+ record.eventSinks.set(sink.id, sink);
118
+ try {
119
+ // Walk the buffer by index so events pushed during an `await`
120
+ // are still picked up in order before we unblock live delivery.
121
+ let cursor = 0;
122
+ while (cursor < record.queuedEvents.length) {
123
+ const event = record.queuedEvents[cursor++];
124
+ if (matchesSinkFilter(sink.filter, event)) {
125
+ await sink.send(event);
126
+ }
127
+ }
128
+ }
129
+ finally {
130
+ sink.pendingReplay = false;
131
+ }
132
+ return record;
133
+ }
134
+ /**
135
+ * Remove an SSE event sink when the connection closes.
136
+ */
137
+ detachEventSink(threadId, sinkId) {
138
+ const record = this.threads.get(threadId);
139
+ record?.eventSinks.delete(sinkId);
140
+ }
141
+ async closeThread(threadId) {
142
+ const record = this.threads.get(threadId);
143
+ if (record == null)
144
+ return;
145
+ // Resolve any still-parked WebSocket subscribes with `no_such_run` so
146
+ // the transport's `onMessage` await unblocks and doesn't leak. The
147
+ // thread is going away before a session was ever bound, so the client
148
+ // will never receive a valid subscription_id anyway.
149
+ const pending = record.pendingSubscribes.splice(0);
150
+ for (const { command, resolve } of pending) {
151
+ resolve({
152
+ type: "error",
153
+ id: command.id,
154
+ error: "no_such_run",
155
+ message: "Thread closed before a run was bound.",
156
+ });
157
+ }
158
+ await record.session?.close();
159
+ this.threads.delete(threadId);
160
+ }
161
+ /**
162
+ * Route a protocol command on a thread.
163
+ *
164
+ * `subscription.subscribe` can resolve to `null` on ordered
165
+ * transports (WebSocket) — see
166
+ * `ProtocolSession.handleSubscribeForResponse` for the rationale.
167
+ * Callers must treat `null` as "response already sent on the wire"
168
+ * and skip any additional send.
169
+ */
170
+ async handleCommand(threadId, command) {
171
+ const record = this.requireThread(threadId);
172
+ switch (command.method) {
173
+ case "run.start":
174
+ return await this.handleRunStart(record, command);
175
+ case "input.respond":
176
+ return await this.handleInputRespond(record, command);
177
+ case "state.get":
178
+ return await this.handleStateGet(record, command);
179
+ default:
180
+ return await this.forwardToRunSession(record, command);
181
+ }
182
+ }
183
+ /**
184
+ * Start a new run, resume an interrupted run, or continue on the
185
+ * thread depending on its current state.
186
+ */
187
+ async handleRunStart(record, command) {
188
+ const params = normalizeRunStart(command.params);
189
+ if (!params.assistant_id) {
190
+ return this.error(command.id, "invalid_argument", "run.start requires an assistant_id.");
191
+ }
192
+ if (record.assistantId != null &&
193
+ record.assistantId !== params.assistant_id) {
194
+ return this.error(command.id, "invalid_argument", `Thread ${record.threadId} is bound to assistant ${record.assistantId}; cannot run ${params.assistant_id}.`);
195
+ }
196
+ record.assistantId = params.assistant_id;
197
+ const run = await this.createOrResumeRun(record, params);
198
+ return {
199
+ type: "success",
200
+ id: command.id,
201
+ result: { run_id: run.run_id },
202
+ meta: {
203
+ thread_id: record.threadId,
204
+ applied_through_seq: record.seq,
205
+ },
206
+ };
207
+ }
208
+ async handleInputRespond(record, command) {
209
+ const params = isRecord(command.params)
210
+ ? command.params
211
+ : {};
212
+ if (typeof params.interrupt_id !== "string") {
213
+ return this.error(command.id, "invalid_argument", "input.respond requires an interrupt_id.");
214
+ }
215
+ if (record.assistantId == null) {
216
+ return this.error(command.id, "no_such_run", "Thread has no active assistant; call run.start first.");
217
+ }
218
+ const currentRun = record.currentRunId != null
219
+ ? await this.bindings.runs.get(record.currentRunId, record.threadId, record.auth)
220
+ : null;
221
+ const hasPendingInterrupts = await this.hasPendingInterrupts(record);
222
+ if (currentRun == null && !hasPendingInterrupts) {
223
+ return this.error(command.id, "no_such_run", "No interrupted run is bound to this thread.");
224
+ }
225
+ if (!hasPendingInterrupts) {
226
+ return this.error(command.id, "invalid_argument", "input.respond can only be used while the run is interrupted.");
227
+ }
228
+ await this.createOrResumeRun(record, {
229
+ assistant_id: record.assistantId,
230
+ input: { [params.interrupt_id]: params.response },
231
+ config: undefined,
232
+ metadata: undefined,
233
+ });
234
+ return {
235
+ type: "success",
236
+ id: command.id,
237
+ result: {},
238
+ meta: {
239
+ thread_id: record.threadId,
240
+ applied_through_seq: record.seq,
241
+ },
242
+ };
243
+ }
244
+ async createOrResumeRun(record, params) {
245
+ const assistantId = getAssistantId(params.assistant_id);
246
+ const currentRun = record.currentRunId != null
247
+ ? await this.bindings.runs.get(record.currentRunId, record.threadId, record.auth)
248
+ : null;
249
+ const currentStatus = currentRun?.status;
250
+ const hasPendingInterrupts = params.input != null
251
+ ? await this.hasPendingInterruptsForThread(record.threadId, record.auth)
252
+ : false;
253
+ const isResume = params.input != null &&
254
+ ((currentRun != null && currentStatus === "interrupted") ||
255
+ hasPendingInterrupts);
256
+ /**
257
+ * When `forkFrom: { checkpointId }` is present, promote it to
258
+ * `configurable.checkpoint_id` so the engine replays from the
259
+ * requested fork target. `forkFrom` is merged last so it wins over
260
+ * any `checkpoint_id` the caller may have pre-baked into
261
+ * `config.configurable`; in resume flows we intentionally skip the
262
+ * promotion because a resume must follow the thread's active
263
+ * checkpoint, not a historical fork.
264
+ */
265
+ const runConfig = {
266
+ ...params.config,
267
+ configurable: {
268
+ ...params.config?.configurable,
269
+ thread_id: record.threadId,
270
+ ...(!isResume && params.forkFrom?.checkpointId != null
271
+ ? { checkpoint_id: params.forkFrom.checkpointId }
272
+ : {}),
273
+ },
274
+ };
275
+ const runPayload = {
276
+ assistant_id: assistantId,
277
+ input: isResume ? null : params.input,
278
+ command: isResume
279
+ ? { resume: params.input }
280
+ : undefined,
281
+ config: runConfig,
282
+ metadata: params.metadata,
283
+ stream_mode: DEFAULT_RUN_STREAM_MODES,
284
+ stream_subgraphs: true,
285
+ stream_resumable: true,
286
+ if_not_exists: "create",
287
+ multitask_strategy: "interrupt",
288
+ };
289
+ const [run] = await this.bindings.runs.put(uuid7(), assistantId, {
290
+ input: runPayload.input,
291
+ command: runPayload.command,
292
+ config: runPayload.config,
293
+ context: undefined,
294
+ stream_mode: runPayload.stream_mode,
295
+ interrupt_before: undefined,
296
+ interrupt_after: undefined,
297
+ temporary: false,
298
+ subgraphs: runPayload.stream_subgraphs,
299
+ resumable: runPayload.stream_resumable,
300
+ [PROTOCOL_STREAM_RUN_KEY]: true,
301
+ }, {
302
+ threadId: record.threadId,
303
+ metadata: runPayload.metadata,
304
+ status: "pending",
305
+ multitaskStrategy: runPayload.multitask_strategy,
306
+ preventInsertInInflight: false,
307
+ ifNotExists: runPayload.if_not_exists,
308
+ }, record.auth);
309
+ await this.ensureRunSession(record, run);
310
+ record.currentRunId = run.run_id;
311
+ // For WebSocket transports, register subscriptions on the session
312
+ // BEFORE starting it so the success responses for parked and sticky
313
+ // subscribes reach the client ahead of the session's first events.
314
+ // `ensureRunSession` intentionally leaves the session unstarted; we
315
+ // kick `session.start()` off after the drain below.
316
+ //
317
+ // 1. Replay sticky subscriptions from previous runs (empty on the
318
+ // first run). These must land first so that parked subs from
319
+ // the current run are appended at the end of
320
+ // `activeSubscriptions` and aren't double-applied here.
321
+ // 2. Drain subscribes that arrived while `record.session` was
322
+ // still null. Their deferred `handleCommand` responses resolve
323
+ // here — the awaiting WebSocket `onMessage` handlers will flush
324
+ // `ws.send(success)` on the next microtask tick, which runs
325
+ // before `session.start()`'s first `pushEvent` completes its
326
+ // `await` chain. The client therefore has every subscription
327
+ // handle registered in `#subscriptions` by the time the run's
328
+ // initial lifecycle/values events arrive.
329
+ if (record.transport === "websocket") {
330
+ for (const cmd of record.activeSubscriptions) {
331
+ await record.session?.handleProtocolCommand(cmd, {
332
+ thread_id: record.threadId,
333
+ applied_through_seq: record.seq,
334
+ }, { deliverResponseInline: true });
335
+ }
336
+ await this.drainPendingSubscribes(record);
337
+ }
338
+ await record.session?.start();
339
+ return run;
340
+ }
341
+ async hasPendingInterrupts(record) {
342
+ return this.hasPendingInterruptsForThread(record.threadId, record.auth);
343
+ }
344
+ async hasPendingInterruptsForThread(threadId, auth) {
345
+ try {
346
+ const state = await this.bindings.threads.state.get({ configurable: { thread_id: threadId } }, { subgraphs: true }, auth);
347
+ return (state.tasks ?? []).some((task) => Array.isArray(task.interrupts) && task.interrupts.length > 0);
348
+ }
349
+ catch {
350
+ return false;
351
+ }
352
+ }
353
+ async handleStateGet(record, command) {
354
+ const params = isRecord(command.params)
355
+ ? command.params
356
+ : {};
357
+ const values = await this.bindings.threads.state.get({ configurable: { thread_id: record.threadId } }, { subgraphs: true }, record.auth);
358
+ const checkpointConfig = isRecord(values.config?.configurable)
359
+ ? values.config.configurable
360
+ : undefined;
361
+ const checkpoint = checkpointConfig != null &&
362
+ typeof checkpointConfig.checkpoint_id === "string"
363
+ ? {
364
+ id: checkpointConfig.checkpoint_id,
365
+ ...(typeof checkpointConfig.checkpoint_ns === "string"
366
+ ? { ns: checkpointConfig.checkpoint_ns }
367
+ : {}),
368
+ }
369
+ : undefined;
370
+ const requestedKeys = normalizeKeys(params.keys);
371
+ const filteredValues = requestedKeys == null
372
+ ? values.values
373
+ : Object.fromEntries(Object.entries(values.values).filter(([key]) => requestedKeys.includes(key)));
374
+ return {
375
+ type: "success",
376
+ id: command.id,
377
+ result: {
378
+ values: filteredValues,
379
+ checkpoint,
380
+ },
381
+ meta: {
382
+ thread_id: record.threadId,
383
+ applied_through_seq: record.seq,
384
+ },
385
+ };
386
+ }
387
+ async forwardToRunSession(record, command) {
388
+ const runSession = record.session;
389
+ if (runSession == null) {
390
+ // WebSocket subscribes can arrive before the concurrent `run.start`
391
+ // has bound a session (the SDK's root pump + legacy lifecycle/values
392
+ // subs are opened eagerly so no events are missed on fast runs).
393
+ // Park the response promise here and resolve it once the first
394
+ // session is bound — see `drainPendingSubscribes`. Mirrors the
395
+ // existing cross-run `activeSubscriptions` replay path.
396
+ if (command.method === "subscription.subscribe" &&
397
+ record.transport === "websocket") {
398
+ return await new Promise((resolve) => {
399
+ record.pendingSubscribes.push({ command, resolve });
400
+ });
401
+ }
402
+ return this.error(command.id, "no_such_run", "No active run is bound to this thread.");
403
+ }
404
+ if (command.method === "subscription.subscribe" &&
405
+ record.transport === "websocket") {
406
+ record.activeSubscriptions.push(command);
407
+ }
408
+ return await runSession.handleProtocolCommand(command, {
409
+ thread_id: record.threadId,
410
+ applied_through_seq: record.seq,
411
+ },
412
+ // WebSocket is an ordered single-channel transport: events and
413
+ // the subscribe response share one wire. Emit the response
414
+ // first via the session's send queue so the client registers
415
+ // the subscription handle before the replay events arrive
416
+ // (otherwise the per-sub fan-out drops them). HTTP `/commands`
417
+ // keeps the default return-the-response behaviour so the
418
+ // response lands in the HTTP body.
419
+ record.transport === "websocket"
420
+ ? { deliverResponseInline: true }
421
+ : undefined);
422
+ }
423
+ /**
424
+ * Drain any WebSocket subscribes that arrived before the first run
425
+ * session was bound. Called from `createOrResumeRun` right after
426
+ * {@link ensureRunSession} sets up `record.session`.
427
+ *
428
+ * Each parked command is forwarded to the freshly-bound session,
429
+ * added to `activeSubscriptions` so it persists across subsequent
430
+ * runs, and its deferred `handleCommand` promise is resolved so the
431
+ * WebSocket handler can finally send the response.
432
+ */
433
+ async drainPendingSubscribes(record) {
434
+ if (record.session == null)
435
+ return;
436
+ if (record.pendingSubscribes.length === 0)
437
+ return;
438
+ const pending = record.pendingSubscribes.splice(0);
439
+ for (const { command, resolve } of pending) {
440
+ record.activeSubscriptions.push(command);
441
+ const response = await record.session.handleProtocolCommand(command, {
442
+ thread_id: record.threadId,
443
+ applied_through_seq: record.seq,
444
+ },
445
+ // Parked subscribes are always WebSocket-origin (see
446
+ // `forwardToRunSession`), so the response must land on the
447
+ // wire before the soon-to-start session's first events.
448
+ { deliverResponseInline: true });
449
+ resolve(response);
450
+ }
451
+ }
452
+ /**
453
+ * Bind the thread record to a concrete LangGraph run and forward
454
+ * normalized protocol events to attached sinks.
455
+ */
456
+ async ensureRunSession(record, run) {
457
+ if (record.session != null && record.currentRunId === run.run_id)
458
+ return;
459
+ await record.session?.close();
460
+ const source = this.bindings.runs.stream.join(run.run_id, run.thread_id, {
461
+ signal: undefined,
462
+ cancelOnDisconnect: false,
463
+ lastEventId: run.kwargs.resumable ? "-1" : undefined,
464
+ }, record.auth);
465
+ const isSSE = record.transport === "sse-http";
466
+ const session = new RunProtocolSession({
467
+ runId: run.run_id,
468
+ threadId: run.thread_id,
469
+ auth: record.auth,
470
+ initialRun: run,
471
+ getRun: () => this.bindings.runs.get(run.run_id, run.thread_id, record.auth),
472
+ getThreadState: async () => await this.bindings.threads.state.get({ configurable: { thread_id: run.thread_id } }, { subgraphs: true }, record.auth),
473
+ source,
474
+ startSeq: record.seq,
475
+ passthrough: isSSE,
476
+ send: async (payload) => {
477
+ const parsed = JSON.parse(payload);
478
+ record.seq = Math.max(record.seq, parsed.seq ?? record.seq);
479
+ if (isSSE) {
480
+ // Always buffer events so late-attaching sinks can replay
481
+ // matching history via attachFilteredEventSink(). Sinks with
482
+ // `pendingReplay` are skipped here — their replay loop will
483
+ // deliver this event in buffer order.
484
+ record.queuedEvents.push(parsed);
485
+ for (const sink of record.eventSinks.values()) {
486
+ if (sink.pendingReplay)
487
+ continue;
488
+ if (matchesSinkFilter(sink.filter, parsed)) {
489
+ await sink.send(parsed);
490
+ }
491
+ }
492
+ }
493
+ else if (record.sendEvent != null) {
494
+ await record.sendEvent(parsed);
495
+ }
496
+ else {
497
+ record.queuedEvents.push(parsed);
498
+ }
499
+ },
500
+ });
501
+ record.session = session;
502
+ // NOTE: `session.start()` is intentionally deferred to the caller
503
+ // (see {@link createOrResumeRun}). On WebSocket we need to drain any
504
+ // parked `subscription.subscribe` commands onto the fresh session —
505
+ // and let the client observe their success responses — BEFORE the
506
+ // session begins emitting events. Otherwise the initial run events
507
+ // are serialised ahead of the subscribe responses on the wire and
508
+ // the client drops them because it hasn't registered the matching
509
+ // subscription handles in `#subscriptions` yet.
510
+ }
511
+ requireThread(threadId) {
512
+ const record = this.threads.get(threadId);
513
+ if (record == null) {
514
+ throw new Error(`No thread record found for ${threadId}`);
515
+ }
516
+ return record;
517
+ }
518
+ error(id, code, message) {
519
+ return {
520
+ type: "error",
521
+ id,
522
+ error: code,
523
+ message,
524
+ };
525
+ }
526
+ }
527
+ function isPrefixMatch(namespace, prefix) {
528
+ if (prefix.length > namespace.length)
529
+ return false;
530
+ return prefix.every((segment, i) => namespace[i] === segment);
531
+ }
532
+ /**
533
+ * Check whether a protocol event matches an SSE event sink filter.
534
+ * Mirrors the subscription matching logic in {@link RunProtocolSession}.
535
+ */
536
+ export function matchesSinkFilter(filter, event) {
537
+ if (filter.since != null && (event.seq ?? 0) <= filter.since)
538
+ return false;
539
+ const channel = event.method === "input.requested"
540
+ ? "input"
541
+ : isSupportedChannel(event.method)
542
+ ? event.method
543
+ : undefined;
544
+ if (channel == null)
545
+ return false;
546
+ let channelMatched = filter.channels.has(channel);
547
+ if (!channelMatched && channel === "custom") {
548
+ const params = event.params;
549
+ const eventName = isRecordInternal(params.data) && typeof params.data.name === "string"
550
+ ? params.data.name
551
+ : undefined;
552
+ if (eventName != null) {
553
+ channelMatched = filter.channels.has(`custom:${eventName}`);
554
+ }
555
+ }
556
+ if (!channelMatched)
557
+ return false;
558
+ if (filter.namespaces == null || filter.namespaces.length === 0) {
559
+ return true;
560
+ }
561
+ return filter.namespaces.some((prefix) => {
562
+ if (!isPrefixMatch(event.params.namespace, prefix))
563
+ return false;
564
+ if (filter.depth == null)
565
+ return true;
566
+ return event.params.namespace.length - prefix.length <= filter.depth;
567
+ });
568
+ }
@@ -0,0 +1,52 @@
1
+ import type { Run } from "../../storage/types.mjs";
2
+ import type { ProtocolEventDataByMethod, ToolsData } from "../types.mjs";
3
+ import type { NormalizedUpdatesData } from "./internal-types.mjs";
4
+ /**
5
+ * Stringifies values without throwing on circular structures.
6
+ *
7
+ * @param value - Value to stringify.
8
+ * @returns A safe string representation.
9
+ */
10
+ export declare const safeStringify: (value: unknown) => string;
11
+ /**
12
+ * Extracts a readable error message from arbitrary payloads.
13
+ *
14
+ * @param value - Error-like payload.
15
+ * @returns A human-readable error string.
16
+ */
17
+ export declare const extractErrorMessage: (value: unknown) => string;
18
+ /**
19
+ * Converts a persisted run status into a protocol lifecycle status.
20
+ *
21
+ * @param status - Run status from storage.
22
+ * @returns The corresponding protocol agent status.
23
+ */
24
+ export declare const toLifecycleStatus: (status: Run["status"]) => "running" | "completed" | "failed" | "interrupted";
25
+ /**
26
+ * Normalizes raw updates payloads into the protocol updates shape.
27
+ *
28
+ * @param value - Raw updates payload.
29
+ * @returns The optional node plus normalized values object.
30
+ */
31
+ export declare const normalizeUpdatesData: (value: unknown) => NormalizedUpdatesData;
32
+ export declare const normalizeInputRequestedData: (value: unknown) => ProtocolEventDataByMethod<"input">[];
33
+ export declare const stripInterruptsFromValues: (value: unknown) => {
34
+ inputRequests: import("@langchain/protocol").InputRequestedData[];
35
+ values: unknown;
36
+ };
37
+ /**
38
+ * Resolves the tool call identifier for a tool event payload.
39
+ *
40
+ * @param value - Raw tool event payload.
41
+ * @param fallbackToolCallId - Generated fallback identifier.
42
+ * @returns The resolved tool call identifier.
43
+ */
44
+ export declare const getToolCallId: (value: Record<string, unknown>, fallbackToolCallId: string) => string;
45
+ /**
46
+ * Normalizes raw tool stream events into protocol tool events.
47
+ *
48
+ * @param value - Raw tool event payload.
49
+ * @param fallbackToolCallId - Generated fallback identifier.
50
+ * @returns A normalized protocol tool event payload.
51
+ */
52
+ export declare const normalizeToolData: (value: unknown, fallbackToolCallId: string) => ToolsData;