@langchain/langgraph-api 1.2.2 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/experimental/embed/protocol.mjs +86 -15
- package/dist/protocol/service.d.mts +9 -1
- package/dist/protocol/service.mjs +65 -25
- package/package.json +6 -6
- package/dist/src/api/assistants.d.mts +0 -3
- package/dist/src/api/assistants.mjs +0 -194
- package/dist/src/api/meta.d.mts +0 -3
- package/dist/src/api/meta.mjs +0 -65
- package/dist/src/api/protocol.d.mts +0 -7
- package/dist/src/api/protocol.mjs +0 -157
- package/dist/src/api/runs.d.mts +0 -3
- package/dist/src/api/runs.mjs +0 -335
- package/dist/src/api/store.d.mts +0 -3
- package/dist/src/api/store.mjs +0 -111
- package/dist/src/api/threads.d.mts +0 -3
- package/dist/src/api/threads.mjs +0 -143
- package/dist/src/graph/load.utils.d.mts +0 -22
- package/dist/src/graph/load.utils.mjs +0 -59
- package/dist/src/protocol/service.d.mts +0 -101
- package/dist/src/protocol/service.mjs +0 -568
- package/dist/src/protocol/session/event-normalizers.d.mts +0 -52
- package/dist/src/protocol/session/index.d.mts +0 -261
- package/dist/src/protocol/session/index.mjs +0 -826
- package/dist/src/protocol/session/namespace.d.mts +0 -47
- package/dist/src/protocol/session/namespace.mjs +0 -62
- package/dist/src/protocol/types.d.mts +0 -121
- package/dist/src/protocol/types.mjs +0 -1
- package/dist/src/schemas.d.mts +0 -1552
- package/dist/src/semver/index.d.mts +0 -15
- package/dist/src/semver/index.mjs +0 -46
- package/dist/src/semver/satisfiesPeerRange.d.mts +0 -1
- package/dist/src/semver/satisfiesPeerRange.mjs +0 -19
- package/dist/src/state.d.mts +0 -3
- package/dist/src/state.mjs +0 -30
- package/dist/src/storage/context.d.mts +0 -3
- package/dist/src/storage/context.mjs +0 -11
- package/dist/src/storage/ops.mjs +0 -1281
- package/dist/src/stream.d.mts +0 -64
- package/dist/src/stream.mjs +0 -427
- package/dist/src/utils/hono.d.mts +0 -5
- package/dist/src/utils/hono.mjs +0 -24
- package/dist/src/utils/runnableConfig.d.mts +0 -3
- package/dist/src/utils/runnableConfig.mjs +0 -45
- package/dist/src/webhook.d.mts +0 -11
- package/dist/src/webhook.mjs +0 -30
|
@@ -1,826 +0,0 @@
|
|
|
1
|
-
import { v7 as uuid7 } from "uuid";
|
|
2
|
-
import { serialiseAsDict, serializeError } from "../../utils/serde.mjs";
|
|
3
|
-
import { normalizeInputRequestedData, normalizeToolData, stripInterruptsFromValues, toLifecycleStatus, } from "./event-normalizers.mjs";
|
|
4
|
-
import { SUPPORTED_CHANNELS, isRecord, isSupportedChannel, } from "./internal-types.mjs";
|
|
5
|
-
import { guessGraphName, isPrefixMatch, normalizeNamespace, parseEventName, toNamespaceKey, } from "./namespace.mjs";
|
|
6
|
-
import { normalizeProtocolStatePayload } from "./state-normalizers.mjs";
|
|
7
|
-
/**
|
|
8
|
-
* Normalizes one LangGraph run into protocol events and manages per-run
|
|
9
|
-
* subscriptions, buffering, and replay.
|
|
10
|
-
*
|
|
11
|
-
* This class is transport-agnostic: callers provide a `send()` function and an
|
|
12
|
-
* optional source stream, and the session handles command processing plus event
|
|
13
|
-
* fan-out for both direct run-scoped sockets and the shared `v2` session
|
|
14
|
-
* service.
|
|
15
|
-
*/
|
|
16
|
-
export class RunProtocolSession {
|
|
17
|
-
initialRun;
|
|
18
|
-
getRun;
|
|
19
|
-
getThreadState;
|
|
20
|
-
send;
|
|
21
|
-
source;
|
|
22
|
-
subscriptions = new Map();
|
|
23
|
-
namespaces = new Map();
|
|
24
|
-
abortController = new AbortController();
|
|
25
|
-
buffer = [];
|
|
26
|
-
sendQueue = Promise.resolve();
|
|
27
|
-
sourceTask;
|
|
28
|
-
nextSeq = 0;
|
|
29
|
-
rootGraphName = "root";
|
|
30
|
-
terminalLifecycleEmitted = false;
|
|
31
|
-
pendingInterruptIds = new Set();
|
|
32
|
-
/**
|
|
33
|
-
* When true, every event is sent unconditionally via {@link sendJson}
|
|
34
|
-
* regardless of subscription state. Used for SSE transports where
|
|
35
|
-
* per-connection filtering is handled by the outer service layer.
|
|
36
|
-
*/
|
|
37
|
-
passthrough;
|
|
38
|
-
/**
|
|
39
|
-
* Creates a run-scoped protocol session.
|
|
40
|
-
*
|
|
41
|
-
* @param options - Session construction options and transport bindings.
|
|
42
|
-
*/
|
|
43
|
-
constructor(options) {
|
|
44
|
-
this.initialRun = options.initialRun;
|
|
45
|
-
this.getRun = options.getRun;
|
|
46
|
-
this.getThreadState = options.getThreadState;
|
|
47
|
-
this.send = options.send;
|
|
48
|
-
this.source = options.source;
|
|
49
|
-
this.passthrough = options.passthrough ?? false;
|
|
50
|
-
if (options.startSeq != null) {
|
|
51
|
-
this.nextSeq = options.startSeq;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Starts consuming the bound run source and seeds the root lifecycle state.
|
|
56
|
-
*/
|
|
57
|
-
async start() {
|
|
58
|
-
this.rootGraphName =
|
|
59
|
-
typeof this.initialRun.kwargs.config?.configurable?.graph_id === "string"
|
|
60
|
-
? this.initialRun.kwargs.config.configurable.graph_id
|
|
61
|
-
: this.initialRun.assistant_id;
|
|
62
|
-
this.setNamespaceInfo([], toLifecycleStatus(this.initialRun.status), {
|
|
63
|
-
graphName: this.rootGraphName,
|
|
64
|
-
});
|
|
65
|
-
await this.pushEvent(this.createEvent("lifecycle", [], {
|
|
66
|
-
event: toLifecycleStatus(this.initialRun.status),
|
|
67
|
-
graph_name: this.rootGraphName,
|
|
68
|
-
}));
|
|
69
|
-
if (this.source != null) {
|
|
70
|
-
this.sourceTask = this.consumeSource();
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Stops consuming the run source and waits for queued writes to settle.
|
|
75
|
-
*/
|
|
76
|
-
async close() {
|
|
77
|
-
this.abortController.abort();
|
|
78
|
-
await this.sourceTask?.catch(() => undefined);
|
|
79
|
-
await this.sendQueue.catch(() => undefined);
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Parses and handles a raw JSON command coming from the transport.
|
|
83
|
-
*
|
|
84
|
-
* @param rawPayload - Raw JSON protocol command payload.
|
|
85
|
-
*/
|
|
86
|
-
async handleCommand(rawPayload) {
|
|
87
|
-
let payload;
|
|
88
|
-
try {
|
|
89
|
-
payload = JSON.parse(rawPayload);
|
|
90
|
-
}
|
|
91
|
-
catch {
|
|
92
|
-
await this.sendError(null, "invalid_argument", "Protocol commands must be valid JSON.");
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
if (!isRecord(payload) ||
|
|
96
|
-
typeof payload.id !== "number" ||
|
|
97
|
-
!Number.isInteger(payload.id) ||
|
|
98
|
-
payload.id < 0 ||
|
|
99
|
-
typeof payload.method !== "string") {
|
|
100
|
-
await this.sendError(null, "invalid_argument", "Protocol commands must include an integer id and string method.");
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
const command = payload;
|
|
104
|
-
try {
|
|
105
|
-
switch (command.method) {
|
|
106
|
-
case "subscription.subscribe":
|
|
107
|
-
await this.handleSubscribe(command);
|
|
108
|
-
return;
|
|
109
|
-
case "subscription.unsubscribe":
|
|
110
|
-
await this.handleUnsubscribe(command);
|
|
111
|
-
return;
|
|
112
|
-
case "agent.getTree":
|
|
113
|
-
await this.sendSuccess(command.id, {
|
|
114
|
-
tree: this.buildTree([]),
|
|
115
|
-
});
|
|
116
|
-
return;
|
|
117
|
-
case "subscription.reconnect":
|
|
118
|
-
await this.sendError(command.id, "not_supported", "subscription.reconnect is not supported by this server yet.");
|
|
119
|
-
return;
|
|
120
|
-
default:
|
|
121
|
-
await this.sendError(command.id, "unknown_command", `Unknown protocol command: ${command.method}`);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
catch (error) {
|
|
125
|
-
await this.sendError(command.id, "unknown_error", error instanceof Error ? error.message : "Unknown protocol error", error instanceof Error ? error.stack : undefined);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Handles a structured protocol command and returns a typed response.
|
|
130
|
-
*
|
|
131
|
-
* When `options.deliverResponseInline` is `true`, a
|
|
132
|
-
* `subscription.subscribe` resolves to `null` — the success response
|
|
133
|
-
* has already been written through the shared transport queue ahead
|
|
134
|
-
* of the replay events (see `handleSubscribeForResponse` for
|
|
135
|
-
* rationale). Callers that forward the response onto a separate wire
|
|
136
|
-
* (the WS `onMessage` handler) must treat `null` as
|
|
137
|
-
* "nothing more to send".
|
|
138
|
-
*
|
|
139
|
-
* @param command - Parsed protocol command.
|
|
140
|
-
* @param meta - Optional response metadata from the outer transport.
|
|
141
|
-
* @param options - Server-internal delivery flags.
|
|
142
|
-
* @returns A typed success/error response, or `null` when the
|
|
143
|
-
* response was already sent inline.
|
|
144
|
-
*/
|
|
145
|
-
async handleProtocolCommand(command, meta, options) {
|
|
146
|
-
try {
|
|
147
|
-
switch (command.method) {
|
|
148
|
-
case "subscription.subscribe":
|
|
149
|
-
return await this.handleSubscribeForResponse(command, meta, options);
|
|
150
|
-
case "subscription.unsubscribe":
|
|
151
|
-
return await this.handleUnsubscribeForResponse(command, meta);
|
|
152
|
-
case "agent.getTree":
|
|
153
|
-
return this.success(command.id, { tree: this.buildTree([]) }, meta);
|
|
154
|
-
case "subscription.reconnect":
|
|
155
|
-
return this.error(command.id, "not_supported", "subscription.reconnect is not supported by this server yet.", meta);
|
|
156
|
-
default:
|
|
157
|
-
return this.error(command.id, "unknown_command", `Unknown protocol command: ${command.method}`, meta);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
catch (error) {
|
|
161
|
-
return this.error(command.id, "unknown_error", error instanceof Error ? error.message : "Unknown protocol error", meta, error instanceof Error ? error.stack : undefined);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Injects a raw run stream event into the protocol session.
|
|
166
|
-
*
|
|
167
|
-
* @param event - Raw source stream event to normalize.
|
|
168
|
-
*/
|
|
169
|
-
async ingestSourceEvent(event) {
|
|
170
|
-
await this.handleSourceEvent(event);
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* Consumes the bound source stream until completion or cancellation.
|
|
174
|
-
*/
|
|
175
|
-
async consumeSource() {
|
|
176
|
-
try {
|
|
177
|
-
for await (const event of this.source ?? []) {
|
|
178
|
-
if (this.abortController.signal.aborted)
|
|
179
|
-
break;
|
|
180
|
-
await this.handleSourceEvent(event);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
catch (error) {
|
|
184
|
-
await this.sendError(null, "unknown_error", error instanceof Error
|
|
185
|
-
? error.message
|
|
186
|
-
: "Failed to consume run stream.", error instanceof Error ? error.stack : undefined);
|
|
187
|
-
}
|
|
188
|
-
finally {
|
|
189
|
-
await this.emitTerminalLifecycle();
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* Emits the terminal root lifecycle event once the underlying run
|
|
194
|
-
* finishes. Child namespaces are cascaded upstream by core's
|
|
195
|
-
* `LifecycleTransformer` — the session only owns the root because
|
|
196
|
-
* terminal status depends on API-only signals (persisted run
|
|
197
|
-
* status, thread-level pending interrupts).
|
|
198
|
-
*/
|
|
199
|
-
async emitTerminalLifecycle() {
|
|
200
|
-
if (this.terminalLifecycleEmitted)
|
|
201
|
-
return;
|
|
202
|
-
const currentRun = await this.getRun();
|
|
203
|
-
if (currentRun == null) {
|
|
204
|
-
this.terminalLifecycleEmitted = true;
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
let status = toLifecycleStatus(currentRun.status);
|
|
208
|
-
if (status === "completed") {
|
|
209
|
-
if (this.pendingInterruptIds.size > 0) {
|
|
210
|
-
status = "interrupted";
|
|
211
|
-
}
|
|
212
|
-
else {
|
|
213
|
-
let threadState;
|
|
214
|
-
try {
|
|
215
|
-
threadState = (await this.getThreadState?.()) ?? null;
|
|
216
|
-
}
|
|
217
|
-
catch {
|
|
218
|
-
threadState = null;
|
|
219
|
-
}
|
|
220
|
-
const hasPendingInterrupts = (threadState?.tasks ?? []).some((task) => Array.isArray(task.interrupts) && task.interrupts.length > 0);
|
|
221
|
-
if (hasPendingInterrupts) {
|
|
222
|
-
status = "interrupted";
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
if (status === "running")
|
|
227
|
-
return;
|
|
228
|
-
this.terminalLifecycleEmitted = true;
|
|
229
|
-
this.setNamespaceInfo([], status, { graphName: this.rootGraphName });
|
|
230
|
-
await this.pushEvent(this.createEvent("lifecycle", [], {
|
|
231
|
-
event: status,
|
|
232
|
-
graph_name: this.rootGraphName,
|
|
233
|
-
}));
|
|
234
|
-
}
|
|
235
|
-
/**
|
|
236
|
-
* Normalizes a single raw source event into protocol events.
|
|
237
|
-
*
|
|
238
|
-
* @param event - Raw source stream event.
|
|
239
|
-
*/
|
|
240
|
-
async handleSourceEvent(event) {
|
|
241
|
-
if (event.event === "metadata")
|
|
242
|
-
return;
|
|
243
|
-
if (event.event === "error") {
|
|
244
|
-
// Core's `LifecycleTransformer.fail()` has already cascaded
|
|
245
|
-
// `lifecycle.failed` to every sub-namespace. The session owns
|
|
246
|
-
// the root lifecycle, so emit only the root here.
|
|
247
|
-
this.terminalLifecycleEmitted = true;
|
|
248
|
-
this.setNamespaceInfo([], "failed", { graphName: this.rootGraphName });
|
|
249
|
-
await this.pushEvent(this.createEvent("lifecycle", [], {
|
|
250
|
-
event: "failed",
|
|
251
|
-
graph_name: this.rootGraphName,
|
|
252
|
-
error: serializeError(event.data).message,
|
|
253
|
-
}));
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
const { method, namespace: rawNamespace } = parseEventName(event.event);
|
|
257
|
-
const namespace = normalizeNamespace(rawNamespace);
|
|
258
|
-
if (event.normalized &&
|
|
259
|
-
(await this.forwardNormalizedSourceEvent(method, namespace, event.data))) {
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
// Authoritative subgraph `lifecycle` events come from core's
|
|
263
|
-
// `LifecycleTransformer`. The session observes them, updates its
|
|
264
|
-
// in-memory namespace tracking for agent-tree queries, and forwards
|
|
265
|
-
// them to subscribers. Root lifecycle events are owned by the
|
|
266
|
-
// session (see `start()` and `emitTerminalLifecycle()`), so root
|
|
267
|
-
// events from core are dropped here.
|
|
268
|
-
if (method === "lifecycle") {
|
|
269
|
-
if (namespace.length === 0)
|
|
270
|
-
return;
|
|
271
|
-
const data = event.data;
|
|
272
|
-
this.setNamespaceInfo(namespace, data.event, {
|
|
273
|
-
graphName: data.graph_name,
|
|
274
|
-
});
|
|
275
|
-
await this.pushEvent(this.createEvent("lifecycle", namespace, data));
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
if (method !== "messages") {
|
|
279
|
-
await this.ensureNamespaces(namespace);
|
|
280
|
-
}
|
|
281
|
-
switch (method) {
|
|
282
|
-
case "values": {
|
|
283
|
-
const normalizedValues = stripInterruptsFromValues(event.data);
|
|
284
|
-
await this.emitInputRequestedEvents(namespace, normalizedValues.inputRequests);
|
|
285
|
-
if (!this.hasStatePayload(normalizedValues.values)) {
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
await this.pushEvent(this.createEvent("values", namespace, normalizedValues.values));
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
case "checkpoints": {
|
|
292
|
-
await this.pushEvent(this.createEvent("checkpoints", namespace, event.data));
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
case "messages":
|
|
296
|
-
return;
|
|
297
|
-
case "custom":
|
|
298
|
-
await this.pushEvent(this.createEvent("custom", namespace, {
|
|
299
|
-
payload: event.data,
|
|
300
|
-
}));
|
|
301
|
-
return;
|
|
302
|
-
case "tasks":
|
|
303
|
-
await this.pushEvent(this.createEvent("tasks", namespace, event.data));
|
|
304
|
-
return;
|
|
305
|
-
case "tools":
|
|
306
|
-
await this.pushEvent(this.createEvent("tools", namespace, normalizeToolData(event.data, event.id ?? uuid7())));
|
|
307
|
-
return;
|
|
308
|
-
default:
|
|
309
|
-
// Route unknown methods as named custom events. This allows
|
|
310
|
-
// reducers to emit("a2a", data) and have it appear on the
|
|
311
|
-
// custom channel with name set for client-side filtering.
|
|
312
|
-
await this.pushEvent(this.createEvent("custom", namespace, {
|
|
313
|
-
name: method,
|
|
314
|
-
payload: event.data,
|
|
315
|
-
}));
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
/**
|
|
320
|
-
* Forwards events already converted by core's
|
|
321
|
-
* `streamEvents(..., { version: "v3" })` pipeline.
|
|
322
|
-
*
|
|
323
|
-
* Only events marked by `streamStateV2` after `convertToProtocolEvent` and
|
|
324
|
-
* the built-in stream transformers have run take this path.
|
|
325
|
-
*/
|
|
326
|
-
async forwardNormalizedSourceEvent(method, namespace, data) {
|
|
327
|
-
switch (method) {
|
|
328
|
-
case "lifecycle": {
|
|
329
|
-
if (namespace.length === 0)
|
|
330
|
-
return true;
|
|
331
|
-
const lifecycleData = data;
|
|
332
|
-
this.setNamespaceInfo(namespace, lifecycleData.event, {
|
|
333
|
-
graphName: lifecycleData.graph_name,
|
|
334
|
-
});
|
|
335
|
-
await this.pushEvent(this.createEvent("lifecycle", namespace, lifecycleData));
|
|
336
|
-
return true;
|
|
337
|
-
}
|
|
338
|
-
case "messages": {
|
|
339
|
-
if (namespace.length > 0) {
|
|
340
|
-
await this.ensureNamespaces(namespace);
|
|
341
|
-
}
|
|
342
|
-
await this.pushEvent(this.createEvent("messages", namespace, data));
|
|
343
|
-
return true;
|
|
344
|
-
}
|
|
345
|
-
case "updates": {
|
|
346
|
-
await this.ensureNamespaces(namespace);
|
|
347
|
-
const updatesData = data;
|
|
348
|
-
if (updatesData.node === "__interrupt__") {
|
|
349
|
-
await this.emitInputRequestedEvents(namespace, normalizeInputRequestedData(updatesData.values));
|
|
350
|
-
return true;
|
|
351
|
-
}
|
|
352
|
-
await this.pushEvent(this.createEvent("updates", namespace, updatesData.values, updatesData.node));
|
|
353
|
-
return true;
|
|
354
|
-
}
|
|
355
|
-
case "tools": {
|
|
356
|
-
await this.ensureNamespaces(namespace);
|
|
357
|
-
await this.pushEvent(this.createEvent("tools", namespace, data));
|
|
358
|
-
return true;
|
|
359
|
-
}
|
|
360
|
-
case "custom": {
|
|
361
|
-
await this.ensureNamespaces(namespace);
|
|
362
|
-
await this.pushEvent(this.createEvent("custom", namespace, data));
|
|
363
|
-
return true;
|
|
364
|
-
}
|
|
365
|
-
case "checkpoints": {
|
|
366
|
-
await this.ensureNamespaces(namespace);
|
|
367
|
-
await this.pushEvent(this.createEvent("checkpoints", namespace, data));
|
|
368
|
-
return true;
|
|
369
|
-
}
|
|
370
|
-
default:
|
|
371
|
-
return false;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
/**
|
|
375
|
-
* Ensures the session is tracking each prefix of `namespace` so
|
|
376
|
-
* agent-tree queries and message routing can resolve it.
|
|
377
|
-
*
|
|
378
|
-
* Does **not** emit any wire events: authoritative
|
|
379
|
-
* `lifecycle.started` events are produced upstream by core's
|
|
380
|
-
* `LifecycleTransformer` and observed by the session's lifecycle
|
|
381
|
-
* handler in {@link RunProtocolSession.handleSourceEvent}. This
|
|
382
|
-
* method is a defensive fallback for event paths that reference a namespace
|
|
383
|
-
* before the corresponding `lifecycle` event has been ingested.
|
|
384
|
-
*
|
|
385
|
-
* @param namespace - Namespace whose prefixes should be tracked.
|
|
386
|
-
*/
|
|
387
|
-
async ensureNamespaces(namespace) {
|
|
388
|
-
for (let length = 1; length <= namespace.length; length += 1) {
|
|
389
|
-
const partial = namespace.slice(0, length);
|
|
390
|
-
const key = toNamespaceKey(partial);
|
|
391
|
-
if (this.namespaces.has(key))
|
|
392
|
-
continue;
|
|
393
|
-
this.setNamespaceInfo(partial, "started", {
|
|
394
|
-
graphName: guessGraphName(partial),
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
/**
|
|
399
|
-
* Updates cached namespace metadata used for lifecycle and tree responses.
|
|
400
|
-
*
|
|
401
|
-
* @param namespace - Namespace to update.
|
|
402
|
-
* @param status - New lifecycle status for the namespace.
|
|
403
|
-
* @param options - Optional graph name override.
|
|
404
|
-
*/
|
|
405
|
-
setNamespaceInfo(namespace, status, options) {
|
|
406
|
-
const key = toNamespaceKey(namespace);
|
|
407
|
-
const existing = this.namespaces.get(key);
|
|
408
|
-
this.namespaces.set(key, {
|
|
409
|
-
namespace,
|
|
410
|
-
status,
|
|
411
|
-
graphName: options?.graphName ??
|
|
412
|
-
existing?.graphName ??
|
|
413
|
-
(namespace.length === 0
|
|
414
|
-
? this.rootGraphName
|
|
415
|
-
: guessGraphName(namespace)),
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
/**
|
|
419
|
-
* Creates a protocol event with sequencing and payload normalization applied.
|
|
420
|
-
*
|
|
421
|
-
* @param method - Event channel to emit.
|
|
422
|
-
* @param namespace - Namespace associated with the event.
|
|
423
|
-
* @param data - Event payload.
|
|
424
|
-
* @param node - Optional node name for updates and messages.
|
|
425
|
-
* @returns A fully-formed protocol event.
|
|
426
|
-
*/
|
|
427
|
-
createEvent(method, namespace, data, node) {
|
|
428
|
-
this.nextSeq += 1;
|
|
429
|
-
const eventMethod = method === "input" ? "input.requested" : method;
|
|
430
|
-
const normalizedData = method === "values" || method === "updates"
|
|
431
|
-
? normalizeProtocolStatePayload(data)
|
|
432
|
-
: data;
|
|
433
|
-
return {
|
|
434
|
-
type: "event",
|
|
435
|
-
event_id: String(this.nextSeq),
|
|
436
|
-
seq: this.nextSeq,
|
|
437
|
-
method: eventMethod,
|
|
438
|
-
params: {
|
|
439
|
-
namespace,
|
|
440
|
-
timestamp: Date.now(),
|
|
441
|
-
...(node != null ? { node } : {}),
|
|
442
|
-
data: normalizedData,
|
|
443
|
-
},
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
/**
|
|
447
|
-
* Buffers an event and delivers it to all matching subscriptions.
|
|
448
|
-
* Applies the active flow-control strategy when the buffer is at capacity.
|
|
449
|
-
*
|
|
450
|
-
* @param event - Protocol event to buffer and fan out.
|
|
451
|
-
*/
|
|
452
|
-
async pushEvent(event) {
|
|
453
|
-
this.buffer.push(event);
|
|
454
|
-
if (this.passthrough) {
|
|
455
|
-
await this.sendJson(event);
|
|
456
|
-
}
|
|
457
|
-
else {
|
|
458
|
-
for (const subscription of this.subscriptions.values()) {
|
|
459
|
-
if (!subscription.active ||
|
|
460
|
-
!this.matchesSubscription(subscription, event)) {
|
|
461
|
-
continue;
|
|
462
|
-
}
|
|
463
|
-
await this.sendJson(event);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
/**
|
|
468
|
-
* Checks whether an event should be delivered to a subscription.
|
|
469
|
-
*
|
|
470
|
-
* @param subscription - Subscription to test.
|
|
471
|
-
* @param event - Candidate protocol event.
|
|
472
|
-
* @returns Whether the event matches channel and namespace filters.
|
|
473
|
-
*/
|
|
474
|
-
matchesSubscription(subscription, event) {
|
|
475
|
-
const channel = event.method === "input.requested"
|
|
476
|
-
? "input"
|
|
477
|
-
: isSupportedChannel(event.method)
|
|
478
|
-
? event.method
|
|
479
|
-
: undefined;
|
|
480
|
-
if (channel == null)
|
|
481
|
-
return false;
|
|
482
|
-
// Support "custom:name" subscriptions: "custom:a2a" matches custom
|
|
483
|
-
// events with data.name === "a2a", while "custom" matches all.
|
|
484
|
-
let channelMatched = subscription.channels.has(channel);
|
|
485
|
-
if (!channelMatched && channel === "custom") {
|
|
486
|
-
const params = event.params;
|
|
487
|
-
const eventName = isRecord(params.data) && typeof params.data.name === "string"
|
|
488
|
-
? params.data.name
|
|
489
|
-
: undefined;
|
|
490
|
-
if (eventName != null) {
|
|
491
|
-
channelMatched = subscription.channels.has(`custom:${eventName}`);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
if (!channelMatched)
|
|
495
|
-
return false;
|
|
496
|
-
if (subscription.namespaces == null ||
|
|
497
|
-
subscription.namespaces.length === 0) {
|
|
498
|
-
return true;
|
|
499
|
-
}
|
|
500
|
-
return subscription.namespaces.some((prefix) => {
|
|
501
|
-
if (!isPrefixMatch(event.params.namespace, prefix))
|
|
502
|
-
return false;
|
|
503
|
-
if (subscription.depth == null)
|
|
504
|
-
return true;
|
|
505
|
-
return (event.params.namespace.length - prefix.length <= subscription.depth);
|
|
506
|
-
});
|
|
507
|
-
}
|
|
508
|
-
hasStatePayload(value) {
|
|
509
|
-
return !isRecord(value) || Object.keys(value).length > 0;
|
|
510
|
-
}
|
|
511
|
-
async emitInputRequestedEvents(namespace, requests) {
|
|
512
|
-
for (const request of requests) {
|
|
513
|
-
if (this.pendingInterruptIds.has(request.interrupt_id)) {
|
|
514
|
-
continue;
|
|
515
|
-
}
|
|
516
|
-
this.pendingInterruptIds.add(request.interrupt_id);
|
|
517
|
-
await this.pushEvent(this.createEvent("input", namespace, request));
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
/**
|
|
521
|
-
* Builds the agent tree view rooted at a namespace.
|
|
522
|
-
*
|
|
523
|
-
* @param namespace - Namespace to build from.
|
|
524
|
-
* @returns A recursively assembled agent tree node.
|
|
525
|
-
*/
|
|
526
|
-
buildTree(namespace) {
|
|
527
|
-
const key = toNamespaceKey(namespace);
|
|
528
|
-
const current = this.namespaces.get(key) ??
|
|
529
|
-
{
|
|
530
|
-
namespace,
|
|
531
|
-
status: "started",
|
|
532
|
-
graphName: namespace.length === 0
|
|
533
|
-
? this.rootGraphName
|
|
534
|
-
: guessGraphName(namespace),
|
|
535
|
-
};
|
|
536
|
-
const children = [...this.namespaces.values()]
|
|
537
|
-
.filter((candidate) => {
|
|
538
|
-
if (candidate.namespace.length !== namespace.length + 1)
|
|
539
|
-
return false;
|
|
540
|
-
return isPrefixMatch(candidate.namespace, namespace);
|
|
541
|
-
})
|
|
542
|
-
.sort((left, right) => JSON.stringify(left.namespace).localeCompare(JSON.stringify(right.namespace)))
|
|
543
|
-
.map((child) => this.buildTree(child.namespace));
|
|
544
|
-
return {
|
|
545
|
-
namespace: current.namespace,
|
|
546
|
-
status: current.status,
|
|
547
|
-
graph_name: current.graphName,
|
|
548
|
-
...(children.length > 0 ? { children } : {}),
|
|
549
|
-
};
|
|
550
|
-
}
|
|
551
|
-
/**
|
|
552
|
-
* Handles a subscribe command and writes the response to the transport.
|
|
553
|
-
*
|
|
554
|
-
* @param command - Subscribe command to process.
|
|
555
|
-
*/
|
|
556
|
-
async handleSubscribe(command) {
|
|
557
|
-
const params = isRecord(command.params)
|
|
558
|
-
? command.params
|
|
559
|
-
: undefined;
|
|
560
|
-
const rawChannels = params?.channels;
|
|
561
|
-
if (!Array.isArray(rawChannels) || rawChannels.length === 0) {
|
|
562
|
-
await this.sendError(command.id, "invalid_argument", "subscription.subscribe requires a non-empty channels array.");
|
|
563
|
-
return;
|
|
564
|
-
}
|
|
565
|
-
const channels = rawChannels.filter((value) => typeof value === "string" &&
|
|
566
|
-
(SUPPORTED_CHANNELS.has(value) ||
|
|
567
|
-
value.startsWith("custom:")));
|
|
568
|
-
if (channels.length !== rawChannels.length) {
|
|
569
|
-
await this.sendError(command.id, "invalid_argument", "subscription.subscribe received an unsupported channel.");
|
|
570
|
-
return;
|
|
571
|
-
}
|
|
572
|
-
const namespaces = Array.isArray(params?.namespaces) &&
|
|
573
|
-
params.namespaces.every((value) => Array.isArray(value) &&
|
|
574
|
-
value.every((segment) => typeof segment === "string"))
|
|
575
|
-
? params.namespaces
|
|
576
|
-
: undefined;
|
|
577
|
-
const depth = typeof params?.depth === "number" &&
|
|
578
|
-
Number.isInteger(params.depth) &&
|
|
579
|
-
params.depth >= 0
|
|
580
|
-
? params.depth
|
|
581
|
-
: undefined;
|
|
582
|
-
const subscription = {
|
|
583
|
-
id: uuid7(),
|
|
584
|
-
channels: new Set(channels),
|
|
585
|
-
namespaces,
|
|
586
|
-
depth,
|
|
587
|
-
active: false,
|
|
588
|
-
};
|
|
589
|
-
this.subscriptions.set(subscription.id, subscription);
|
|
590
|
-
const snapshotSeq = this.nextSeq;
|
|
591
|
-
const snapshot = this.buffer.filter((event) => (event.seq ?? 0) <= snapshotSeq &&
|
|
592
|
-
this.matchesSubscription(subscription, event));
|
|
593
|
-
await this.sendSuccess(command.id, {
|
|
594
|
-
subscription_id: subscription.id,
|
|
595
|
-
replayed_events: snapshot.length,
|
|
596
|
-
});
|
|
597
|
-
for (const event of snapshot) {
|
|
598
|
-
await this.sendJson(event);
|
|
599
|
-
}
|
|
600
|
-
let cursor = snapshotSeq;
|
|
601
|
-
while (true) {
|
|
602
|
-
const drain = this.buffer.filter((event) => (event.seq ?? 0) > cursor &&
|
|
603
|
-
this.matchesSubscription(subscription, event));
|
|
604
|
-
if (drain.length === 0)
|
|
605
|
-
break;
|
|
606
|
-
for (const event of drain) {
|
|
607
|
-
await this.sendJson(event);
|
|
608
|
-
}
|
|
609
|
-
cursor = drain.at(-1)?.seq ?? cursor;
|
|
610
|
-
}
|
|
611
|
-
subscription.active = true;
|
|
612
|
-
}
|
|
613
|
-
/**
|
|
614
|
-
* Handles a subscribe command and (optionally) writes the
|
|
615
|
-
* success/error response inline through the shared transport
|
|
616
|
-
* queue, signalling to callers (via a `null` return) that no
|
|
617
|
-
* further send is required.
|
|
618
|
-
*
|
|
619
|
-
* When `options.deliverResponseInline` is `true`, the success
|
|
620
|
-
* response is emitted via `sendJson` BEFORE the replay events and
|
|
621
|
-
* the method returns `null`. This ordering is critical on
|
|
622
|
-
* single-channel transports like WebSocket where the command
|
|
623
|
-
* response and event frames share one ordered wire: if events
|
|
624
|
-
* preceded the response, the client would receive events whose
|
|
625
|
-
* `subscription_id` it hasn't yet registered (the awaiter in
|
|
626
|
-
* `#subscribeViaCommand` only adds the subscription to its local
|
|
627
|
-
* map after the response resolves) and drop them in the per-sub
|
|
628
|
-
* fan-out.
|
|
629
|
-
*
|
|
630
|
-
* When `deliverResponseInline` is absent/`false` (the default, used
|
|
631
|
-
* by HTTP `/commands`), the response is returned so the caller can
|
|
632
|
-
* place it in the HTTP response body, matching the prior behaviour.
|
|
633
|
-
* Input-validation errors always return normally regardless of the
|
|
634
|
-
* flag so they surface consistently in both modes.
|
|
635
|
-
*
|
|
636
|
-
* @param command - Subscribe command to process.
|
|
637
|
-
* @param meta - Optional response metadata from the outer transport.
|
|
638
|
-
* @param options - Server-internal delivery flags.
|
|
639
|
-
* @returns `null` when the response was sent inline; a typed
|
|
640
|
-
* success/error response otherwise.
|
|
641
|
-
*/
|
|
642
|
-
async handleSubscribeForResponse(command, meta, options) {
|
|
643
|
-
const deliverResponseInline = options?.deliverResponseInline === true;
|
|
644
|
-
const params = isRecord(command.params)
|
|
645
|
-
? command.params
|
|
646
|
-
: undefined;
|
|
647
|
-
const rawChannels = params?.channels;
|
|
648
|
-
if (!Array.isArray(rawChannels) || rawChannels.length === 0) {
|
|
649
|
-
return this.error(command.id, "invalid_argument", "subscription.subscribe requires a non-empty channels array.", meta);
|
|
650
|
-
}
|
|
651
|
-
const channels = rawChannels.filter((value) => typeof value === "string" &&
|
|
652
|
-
(SUPPORTED_CHANNELS.has(value) ||
|
|
653
|
-
value.startsWith("custom:")));
|
|
654
|
-
if (channels.length !== rawChannels.length) {
|
|
655
|
-
return this.error(command.id, "invalid_argument", "subscription.subscribe received an unsupported channel.", meta);
|
|
656
|
-
}
|
|
657
|
-
const namespaces = Array.isArray(params?.namespaces) &&
|
|
658
|
-
params.namespaces.every((value) => Array.isArray(value) &&
|
|
659
|
-
value.every((segment) => typeof segment === "string"))
|
|
660
|
-
? params.namespaces
|
|
661
|
-
: undefined;
|
|
662
|
-
const depth = typeof params?.depth === "number" &&
|
|
663
|
-
Number.isInteger(params.depth) &&
|
|
664
|
-
params.depth >= 0
|
|
665
|
-
? params.depth
|
|
666
|
-
: undefined;
|
|
667
|
-
const subscription = {
|
|
668
|
-
id: uuid7(),
|
|
669
|
-
channels: new Set(channels),
|
|
670
|
-
namespaces,
|
|
671
|
-
depth,
|
|
672
|
-
active: false,
|
|
673
|
-
};
|
|
674
|
-
this.subscriptions.set(subscription.id, subscription);
|
|
675
|
-
const snapshotSeq = this.nextSeq;
|
|
676
|
-
const snapshot = this.buffer.filter((event) => (event.seq ?? 0) <= snapshotSeq &&
|
|
677
|
-
this.matchesSubscription(subscription, event));
|
|
678
|
-
const responsePayload = {
|
|
679
|
-
type: "success",
|
|
680
|
-
id: command.id,
|
|
681
|
-
result: {
|
|
682
|
-
subscription_id: subscription.id,
|
|
683
|
-
replayed_events: snapshot.length,
|
|
684
|
-
},
|
|
685
|
-
...(meta != null ? { meta } : {}),
|
|
686
|
-
};
|
|
687
|
-
if (deliverResponseInline) {
|
|
688
|
-
// Response first, replay events second — see method-level
|
|
689
|
-
// comment on why the ordering matters for ordered transports.
|
|
690
|
-
await this.sendJson(responsePayload);
|
|
691
|
-
}
|
|
692
|
-
for (const event of snapshot) {
|
|
693
|
-
await this.sendJson(event);
|
|
694
|
-
}
|
|
695
|
-
let cursor = snapshotSeq;
|
|
696
|
-
while (true) {
|
|
697
|
-
const drain = this.buffer.filter((event) => (event.seq ?? 0) > cursor &&
|
|
698
|
-
this.matchesSubscription(subscription, event));
|
|
699
|
-
if (drain.length === 0)
|
|
700
|
-
break;
|
|
701
|
-
for (const event of drain) {
|
|
702
|
-
await this.sendJson(event);
|
|
703
|
-
}
|
|
704
|
-
cursor = drain.at(-1)?.seq ?? cursor;
|
|
705
|
-
}
|
|
706
|
-
subscription.active = true;
|
|
707
|
-
return deliverResponseInline ? null : responsePayload;
|
|
708
|
-
}
|
|
709
|
-
/**
|
|
710
|
-
* Handles an unsubscribe command and writes the response to the transport.
|
|
711
|
-
*
|
|
712
|
-
* @param command - Unsubscribe command to process.
|
|
713
|
-
*/
|
|
714
|
-
async handleUnsubscribe(command) {
|
|
715
|
-
const params = isRecord(command.params)
|
|
716
|
-
? command.params
|
|
717
|
-
: undefined;
|
|
718
|
-
const subscriptionId = params?.subscription_id;
|
|
719
|
-
if (typeof subscriptionId !== "string") {
|
|
720
|
-
await this.sendError(command.id, "invalid_argument", "subscription.unsubscribe requires a subscription_id.");
|
|
721
|
-
return;
|
|
722
|
-
}
|
|
723
|
-
if (!this.subscriptions.delete(subscriptionId)) {
|
|
724
|
-
await this.sendError(command.id, "no_such_subscription", `Unknown subscription: ${subscriptionId}`);
|
|
725
|
-
return;
|
|
726
|
-
}
|
|
727
|
-
await this.sendSuccess(command.id, {});
|
|
728
|
-
}
|
|
729
|
-
/**
|
|
730
|
-
* Handles an unsubscribe command and returns a typed response.
|
|
731
|
-
*
|
|
732
|
-
* @param command - Unsubscribe command to process.
|
|
733
|
-
* @param meta - Optional response metadata from the outer transport.
|
|
734
|
-
* @returns A typed success or error response.
|
|
735
|
-
*/
|
|
736
|
-
async handleUnsubscribeForResponse(command, meta) {
|
|
737
|
-
const params = isRecord(command.params)
|
|
738
|
-
? command.params
|
|
739
|
-
: undefined;
|
|
740
|
-
const subscriptionId = params?.subscription_id;
|
|
741
|
-
if (typeof subscriptionId !== "string") {
|
|
742
|
-
return this.error(command.id, "invalid_argument", "subscription.unsubscribe requires a subscription_id.", meta);
|
|
743
|
-
}
|
|
744
|
-
if (!this.subscriptions.delete(subscriptionId)) {
|
|
745
|
-
return this.error(command.id, "no_such_subscription", `Unknown subscription: ${subscriptionId}`, meta);
|
|
746
|
-
}
|
|
747
|
-
return this.success(command.id, {}, meta);
|
|
748
|
-
}
|
|
749
|
-
/**
|
|
750
|
-
* Sends a success response over the bound transport.
|
|
751
|
-
*
|
|
752
|
-
* @param id - Command identifier being acknowledged.
|
|
753
|
-
* @param result - Typed success payload.
|
|
754
|
-
*/
|
|
755
|
-
async sendSuccess(id, result) {
|
|
756
|
-
await this.sendJson({
|
|
757
|
-
type: "success",
|
|
758
|
-
id,
|
|
759
|
-
result,
|
|
760
|
-
});
|
|
761
|
-
}
|
|
762
|
-
/**
|
|
763
|
-
* Sends an error response over the bound transport.
|
|
764
|
-
*
|
|
765
|
-
* @param id - Command identifier, when available.
|
|
766
|
-
* @param error - Protocol error code.
|
|
767
|
-
* @param message - Human-readable error message.
|
|
768
|
-
* @param stacktrace - Optional stack trace for debugging.
|
|
769
|
-
*/
|
|
770
|
-
async sendError(id, error, message, stacktrace) {
|
|
771
|
-
await this.sendJson({
|
|
772
|
-
type: "error",
|
|
773
|
-
id,
|
|
774
|
-
error,
|
|
775
|
-
message,
|
|
776
|
-
...(stacktrace != null ? { stacktrace } : {}),
|
|
777
|
-
});
|
|
778
|
-
}
|
|
779
|
-
/**
|
|
780
|
-
* Serializes and writes a protocol payload using the session transport queue.
|
|
781
|
-
*
|
|
782
|
-
* @param message - Protocol message to send.
|
|
783
|
-
*/
|
|
784
|
-
async sendJson(message) {
|
|
785
|
-
this.sendQueue = this.sendQueue
|
|
786
|
-
.then(() => this.send(serialiseAsDict(message)))
|
|
787
|
-
.catch(() => undefined);
|
|
788
|
-
await this.sendQueue;
|
|
789
|
-
}
|
|
790
|
-
/**
|
|
791
|
-
* Creates a typed success response object.
|
|
792
|
-
*
|
|
793
|
-
* @param id - Command identifier being acknowledged.
|
|
794
|
-
* @param result - Typed success payload.
|
|
795
|
-
* @param meta - Optional response metadata from the outer transport.
|
|
796
|
-
* @returns A typed protocol success response.
|
|
797
|
-
*/
|
|
798
|
-
success(id, result, meta) {
|
|
799
|
-
return {
|
|
800
|
-
type: "success",
|
|
801
|
-
id,
|
|
802
|
-
result,
|
|
803
|
-
...(meta != null ? { meta } : {}),
|
|
804
|
-
};
|
|
805
|
-
}
|
|
806
|
-
/**
|
|
807
|
-
* Creates a typed error response object.
|
|
808
|
-
*
|
|
809
|
-
* @param id - Command identifier, when available.
|
|
810
|
-
* @param error - Protocol error code.
|
|
811
|
-
* @param message - Human-readable error message.
|
|
812
|
-
* @param meta - Optional response metadata from the outer transport.
|
|
813
|
-
* @param stacktrace - Optional stack trace for debugging.
|
|
814
|
-
* @returns A typed protocol error response.
|
|
815
|
-
*/
|
|
816
|
-
error(id, error, message, meta, stacktrace) {
|
|
817
|
-
return {
|
|
818
|
-
type: "error",
|
|
819
|
-
id,
|
|
820
|
-
error,
|
|
821
|
-
message,
|
|
822
|
-
...(stacktrace != null ? { stacktrace } : {}),
|
|
823
|
-
...(meta != null ? { meta } : {}),
|
|
824
|
-
};
|
|
825
|
-
}
|
|
826
|
-
}
|