@macrow/copilotkit-langgraph-history 0.1.7
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 +21 -0
- package/README.md +308 -0
- package/dist/index.cjs +947 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +647 -0
- package/dist/index.d.ts +647 -0
- package/dist/index.js +936 -0
- package/dist/index.js.map +1 -0
- package/package.json +101 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,936 @@
|
|
|
1
|
+
import { EventType } from '@ag-ui/core';
|
|
2
|
+
import { LangGraphAgent } from '@copilotkit/runtime/langgraph';
|
|
3
|
+
import { AgentRunner } from '@copilotkitnext/runtime';
|
|
4
|
+
import { Client } from '@langchain/langgraph-sdk';
|
|
5
|
+
import { Observable } from 'rxjs';
|
|
6
|
+
|
|
7
|
+
// src/runner/history-hydrating-runner.ts
|
|
8
|
+
|
|
9
|
+
// src/runner/constants.ts
|
|
10
|
+
var DEFAULT_TIMEOUT = 30 * 60 * 1e3;
|
|
11
|
+
var DEFAULT_HISTORY_LIMIT = 100;
|
|
12
|
+
var MAX_HISTORY_LIMIT = 1e3;
|
|
13
|
+
function createIsolatedAgent(config) {
|
|
14
|
+
const timeout = config.clientTimeoutMs ?? DEFAULT_TIMEOUT;
|
|
15
|
+
const isolatedConfig = Object.freeze({
|
|
16
|
+
deploymentUrl: String(config.deploymentUrl),
|
|
17
|
+
graphId: String(config.graphId),
|
|
18
|
+
langsmithApiKey: config.langsmithApiKey ? String(config.langsmithApiKey) : void 0,
|
|
19
|
+
debug: Boolean(config.debug)
|
|
20
|
+
});
|
|
21
|
+
const agent = new LangGraphAgent(isolatedConfig);
|
|
22
|
+
const clientInternals = agent.client;
|
|
23
|
+
const expectedUrl = config.deploymentUrl.replace(/\/$/, "");
|
|
24
|
+
const actualUrl = clientInternals.apiUrl?.replace(/\/$/, "");
|
|
25
|
+
if (expectedUrl !== actualUrl) {
|
|
26
|
+
console.warn(
|
|
27
|
+
`[LangGraphHistory] URL mismatch detected! Expected: ${expectedUrl}, Got: ${actualUrl}. Replacing client.`
|
|
28
|
+
);
|
|
29
|
+
const newClient = new Client({
|
|
30
|
+
apiUrl: config.deploymentUrl,
|
|
31
|
+
apiKey: config.langsmithApiKey,
|
|
32
|
+
timeoutMs: timeout,
|
|
33
|
+
onRequest: config.onRequest
|
|
34
|
+
});
|
|
35
|
+
Object.assign(agent, { client: newClient });
|
|
36
|
+
}
|
|
37
|
+
return agent;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/utils/message-transformer.ts
|
|
41
|
+
function extractContent(content) {
|
|
42
|
+
if (typeof content === "string") {
|
|
43
|
+
return content;
|
|
44
|
+
}
|
|
45
|
+
if (Array.isArray(content)) {
|
|
46
|
+
return content.map((block) => {
|
|
47
|
+
if (block.type === "text" && block.text) {
|
|
48
|
+
return block.text;
|
|
49
|
+
}
|
|
50
|
+
return "";
|
|
51
|
+
}).filter((text) => text.length > 0).join("\n");
|
|
52
|
+
}
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
function transformMessages(messages, options) {
|
|
56
|
+
const result = [];
|
|
57
|
+
for (const msg of messages) {
|
|
58
|
+
try {
|
|
59
|
+
let transformed = null;
|
|
60
|
+
switch (msg.type) {
|
|
61
|
+
case "human": {
|
|
62
|
+
const content = extractContent(msg.content);
|
|
63
|
+
transformed = {
|
|
64
|
+
id: msg.id,
|
|
65
|
+
role: "user",
|
|
66
|
+
content
|
|
67
|
+
};
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
case "ai": {
|
|
71
|
+
const content = extractContent(msg.content);
|
|
72
|
+
transformed = {
|
|
73
|
+
id: msg.id,
|
|
74
|
+
role: "assistant",
|
|
75
|
+
content: content || "",
|
|
76
|
+
toolCalls: msg.tool_calls?.map((toolCall) => ({
|
|
77
|
+
id: toolCall.id,
|
|
78
|
+
type: "function",
|
|
79
|
+
function: {
|
|
80
|
+
name: toolCall.name,
|
|
81
|
+
arguments: JSON.stringify(toolCall.args)
|
|
82
|
+
}
|
|
83
|
+
}))
|
|
84
|
+
};
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
case "system": {
|
|
88
|
+
const content = extractContent(msg.content);
|
|
89
|
+
transformed = {
|
|
90
|
+
id: msg.id,
|
|
91
|
+
role: "system",
|
|
92
|
+
content
|
|
93
|
+
};
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
case "tool": {
|
|
97
|
+
const content = extractContent(msg.content);
|
|
98
|
+
transformed = {
|
|
99
|
+
id: msg.id,
|
|
100
|
+
role: "tool",
|
|
101
|
+
content,
|
|
102
|
+
toolCallId: msg.tool_call_id
|
|
103
|
+
};
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
default:
|
|
107
|
+
if (options?.debug) {
|
|
108
|
+
console.warn(
|
|
109
|
+
`[HistoryHydratingRunner] Unknown message type: ${msg.type}`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (transformed) {
|
|
114
|
+
result.push(transformed);
|
|
115
|
+
}
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (options?.debug) {
|
|
118
|
+
console.warn(
|
|
119
|
+
"[HistoryHydratingRunner] Failed to transform message:",
|
|
120
|
+
msg,
|
|
121
|
+
error
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/events/custom-events.ts
|
|
130
|
+
var CustomEventNames = /* @__PURE__ */ ((CustomEventNames2) => {
|
|
131
|
+
CustomEventNames2["CopilotKitManuallyEmitMessage"] = "copilotkit_manually_emit_message";
|
|
132
|
+
CustomEventNames2["CopilotKitManuallyEmitToolCall"] = "copilotkit_manually_emit_tool_call";
|
|
133
|
+
CustomEventNames2["CopilotKitManuallyEmitIntermediateState"] = "copilotkit_manually_emit_intermediate_state";
|
|
134
|
+
CustomEventNames2["CopilotKitExit"] = "copilotkit_exit";
|
|
135
|
+
return CustomEventNames2;
|
|
136
|
+
})(CustomEventNames || {});
|
|
137
|
+
|
|
138
|
+
// src/events/langgraph-events.ts
|
|
139
|
+
var LangGraphEventTypes = /* @__PURE__ */ ((LangGraphEventTypes2) => {
|
|
140
|
+
LangGraphEventTypes2["OnChatModelStart"] = "on_chat_model_start";
|
|
141
|
+
LangGraphEventTypes2["OnChatModelStream"] = "on_chat_model_stream";
|
|
142
|
+
LangGraphEventTypes2["OnChatModelEnd"] = "on_chat_model_end";
|
|
143
|
+
LangGraphEventTypes2["OnToolStart"] = "on_tool_start";
|
|
144
|
+
LangGraphEventTypes2["OnToolEnd"] = "on_tool_end";
|
|
145
|
+
LangGraphEventTypes2["OnChainStart"] = "on_chain_start";
|
|
146
|
+
LangGraphEventTypes2["OnChainEnd"] = "on_chain_end";
|
|
147
|
+
return LangGraphEventTypes2;
|
|
148
|
+
})(LangGraphEventTypes || {});
|
|
149
|
+
|
|
150
|
+
// src/utils/stream-processor.ts
|
|
151
|
+
async function processStreamChunk(chunk, context) {
|
|
152
|
+
const { event, data } = chunk;
|
|
153
|
+
let { runId } = context;
|
|
154
|
+
const { threadId, subscriber, startedMessages, startedToolCalls, debug } = context;
|
|
155
|
+
let manuallyEmittedState = context.manuallyEmittedState;
|
|
156
|
+
switch (event) {
|
|
157
|
+
case "metadata": {
|
|
158
|
+
const metadataData = data;
|
|
159
|
+
if (metadataData.run_id) {
|
|
160
|
+
runId = metadataData.run_id;
|
|
161
|
+
}
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
case "events": {
|
|
165
|
+
const eventsData = data;
|
|
166
|
+
const rawEvent = {
|
|
167
|
+
type: "RAW",
|
|
168
|
+
event: eventsData.event,
|
|
169
|
+
name: eventsData.name,
|
|
170
|
+
data: eventsData.data,
|
|
171
|
+
run_id: eventsData.run_id,
|
|
172
|
+
metadata: chunk.metadata,
|
|
173
|
+
rawEvent: {
|
|
174
|
+
id: runId,
|
|
175
|
+
event: eventsData.event,
|
|
176
|
+
name: eventsData.name,
|
|
177
|
+
data: eventsData.data,
|
|
178
|
+
run_id: eventsData.run_id,
|
|
179
|
+
metadata: chunk.metadata
|
|
180
|
+
},
|
|
181
|
+
timestamp: Date.now(),
|
|
182
|
+
threadId,
|
|
183
|
+
runId
|
|
184
|
+
};
|
|
185
|
+
const eventType = eventsData.event;
|
|
186
|
+
const toolCallData = eventsData.data?.chunk?.tool_call_chunks?.[0];
|
|
187
|
+
const metadata = chunk.metadata || {};
|
|
188
|
+
const emitIntermediateState = metadata["copilotkit:emit-intermediate-state"];
|
|
189
|
+
const toolCallUsedToPredictState = emitIntermediateState?.some(
|
|
190
|
+
(predictStateTool) => predictStateTool.tool === toolCallData?.name
|
|
191
|
+
);
|
|
192
|
+
if (eventType === "on_chat_model_stream" /* OnChatModelStream */ && toolCallUsedToPredictState) {
|
|
193
|
+
subscriber.next({
|
|
194
|
+
type: "CUSTOM",
|
|
195
|
+
name: "PredictState",
|
|
196
|
+
value: metadata["copilotkit:emit-intermediate-state"],
|
|
197
|
+
rawEvent,
|
|
198
|
+
timestamp: Date.now(),
|
|
199
|
+
threadId,
|
|
200
|
+
runId
|
|
201
|
+
});
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
if (eventType === "on_chat_model_stream" /* OnChatModelStream */) {
|
|
205
|
+
const messageChunk = eventsData.data?.chunk;
|
|
206
|
+
if (messageChunk?.content) {
|
|
207
|
+
if ("copilotkit:emit-messages" in metadata && metadata["copilotkit:emit-messages"] === false) {
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
const messageId = eventsData.run_id || runId;
|
|
211
|
+
const delta = typeof messageChunk.content === "string" ? messageChunk.content : "";
|
|
212
|
+
if (startedMessages && !startedMessages.has(messageId)) {
|
|
213
|
+
subscriber.next({
|
|
214
|
+
type: "TEXT_MESSAGE_START",
|
|
215
|
+
role: "assistant",
|
|
216
|
+
messageId,
|
|
217
|
+
rawEvent,
|
|
218
|
+
timestamp: Date.now(),
|
|
219
|
+
threadId,
|
|
220
|
+
runId
|
|
221
|
+
});
|
|
222
|
+
startedMessages.add(messageId);
|
|
223
|
+
}
|
|
224
|
+
subscriber.next({
|
|
225
|
+
type: "TEXT_MESSAGE_CONTENT",
|
|
226
|
+
messageId,
|
|
227
|
+
delta,
|
|
228
|
+
rawEvent,
|
|
229
|
+
timestamp: Date.now(),
|
|
230
|
+
threadId,
|
|
231
|
+
runId
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (eventType === "on_chat_model_start" /* OnChatModelStart */) {
|
|
236
|
+
const eventMetadata = chunk.metadata || {};
|
|
237
|
+
if ("copilotkit:emit-messages" in eventMetadata && eventMetadata["copilotkit:emit-messages"] === false) {
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
const messageId = eventsData.run_id || runId;
|
|
241
|
+
if (!startedMessages || !startedMessages.has(messageId)) {
|
|
242
|
+
subscriber.next({
|
|
243
|
+
type: "TEXT_MESSAGE_START",
|
|
244
|
+
role: "assistant",
|
|
245
|
+
messageId,
|
|
246
|
+
rawEvent,
|
|
247
|
+
timestamp: Date.now(),
|
|
248
|
+
threadId,
|
|
249
|
+
runId
|
|
250
|
+
});
|
|
251
|
+
if (startedMessages) {
|
|
252
|
+
startedMessages.add(messageId);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (eventType === "on_chat_model_end" /* OnChatModelEnd */) {
|
|
257
|
+
const eventMetadata = chunk.metadata || {};
|
|
258
|
+
if ("copilotkit:emit-messages" in eventMetadata && eventMetadata["copilotkit:emit-messages"] === false) {
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
const messageId = eventsData.run_id || runId;
|
|
262
|
+
if (startedMessages && !startedMessages.has(messageId)) {
|
|
263
|
+
subscriber.next({
|
|
264
|
+
type: "TEXT_MESSAGE_START",
|
|
265
|
+
role: "assistant",
|
|
266
|
+
messageId,
|
|
267
|
+
rawEvent,
|
|
268
|
+
timestamp: Date.now(),
|
|
269
|
+
threadId,
|
|
270
|
+
runId
|
|
271
|
+
});
|
|
272
|
+
startedMessages.add(messageId);
|
|
273
|
+
}
|
|
274
|
+
subscriber.next({
|
|
275
|
+
type: "TEXT_MESSAGE_END",
|
|
276
|
+
messageId,
|
|
277
|
+
rawEvent,
|
|
278
|
+
timestamp: Date.now(),
|
|
279
|
+
threadId,
|
|
280
|
+
runId
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
if (eventType === "on_tool_start" /* OnToolStart */) {
|
|
284
|
+
const eventMetadata = chunk.metadata || {};
|
|
285
|
+
if ("copilotkit:emit-tool-calls" in eventMetadata && eventMetadata["copilotkit:emit-tool-calls"] === false) {
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
const toolData = eventsData.data?.input;
|
|
289
|
+
const toolName = eventsData.name;
|
|
290
|
+
const toolCallId = eventsData.run_id || runId;
|
|
291
|
+
if (!startedToolCalls || !startedToolCalls.has(toolCallId)) {
|
|
292
|
+
subscriber.next({
|
|
293
|
+
type: "TOOL_CALL_START",
|
|
294
|
+
toolCallId,
|
|
295
|
+
toolCallName: toolName,
|
|
296
|
+
parentMessageId: runId,
|
|
297
|
+
rawEvent,
|
|
298
|
+
timestamp: Date.now(),
|
|
299
|
+
threadId,
|
|
300
|
+
runId
|
|
301
|
+
});
|
|
302
|
+
if (startedToolCalls) {
|
|
303
|
+
startedToolCalls.add(toolCallId);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (toolData) {
|
|
307
|
+
subscriber.next({
|
|
308
|
+
type: "TOOL_CALL_ARGS",
|
|
309
|
+
toolCallId,
|
|
310
|
+
delta: JSON.stringify(toolData),
|
|
311
|
+
rawEvent,
|
|
312
|
+
timestamp: Date.now(),
|
|
313
|
+
threadId,
|
|
314
|
+
runId
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (eventType === "on_tool_end" /* OnToolEnd */) {
|
|
319
|
+
const eventMetadata = chunk.metadata || {};
|
|
320
|
+
if ("copilotkit:emit-tool-calls" in eventMetadata && eventMetadata["copilotkit:emit-tool-calls"] === false) {
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
const toolCallId = eventsData.run_id || runId;
|
|
324
|
+
const toolName = eventsData.name;
|
|
325
|
+
if (startedToolCalls && !startedToolCalls.has(toolCallId)) {
|
|
326
|
+
subscriber.next({
|
|
327
|
+
type: "TOOL_CALL_START",
|
|
328
|
+
toolCallId,
|
|
329
|
+
toolCallName: toolName,
|
|
330
|
+
parentMessageId: runId,
|
|
331
|
+
rawEvent,
|
|
332
|
+
timestamp: Date.now(),
|
|
333
|
+
threadId,
|
|
334
|
+
runId
|
|
335
|
+
});
|
|
336
|
+
startedToolCalls.add(toolCallId);
|
|
337
|
+
}
|
|
338
|
+
subscriber.next({
|
|
339
|
+
type: "TOOL_CALL_END",
|
|
340
|
+
toolCallId,
|
|
341
|
+
rawEvent,
|
|
342
|
+
timestamp: Date.now(),
|
|
343
|
+
threadId,
|
|
344
|
+
runId
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
subscriber.next({
|
|
348
|
+
type: "CUSTOM",
|
|
349
|
+
name: eventsData.event,
|
|
350
|
+
value: JSON.stringify(eventsData.data),
|
|
351
|
+
rawEvent,
|
|
352
|
+
timestamp: Date.now(),
|
|
353
|
+
threadId,
|
|
354
|
+
runId
|
|
355
|
+
});
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
case "updates": {
|
|
359
|
+
const updatesData = data;
|
|
360
|
+
subscriber.next({
|
|
361
|
+
type: "STATE_SNAPSHOT",
|
|
362
|
+
snapshot: updatesData,
|
|
363
|
+
rawEvent: {
|
|
364
|
+
id: runId,
|
|
365
|
+
event: "updates",
|
|
366
|
+
data: updatesData
|
|
367
|
+
},
|
|
368
|
+
timestamp: Date.now(),
|
|
369
|
+
threadId,
|
|
370
|
+
runId
|
|
371
|
+
});
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
case "values": {
|
|
375
|
+
const valuesData = data;
|
|
376
|
+
subscriber.next({
|
|
377
|
+
type: "STATE_SNAPSHOT",
|
|
378
|
+
snapshot: valuesData,
|
|
379
|
+
rawEvent: {
|
|
380
|
+
id: runId,
|
|
381
|
+
event: "values",
|
|
382
|
+
data: valuesData
|
|
383
|
+
},
|
|
384
|
+
timestamp: Date.now(),
|
|
385
|
+
threadId,
|
|
386
|
+
runId
|
|
387
|
+
});
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
case "custom": {
|
|
391
|
+
const customData = data;
|
|
392
|
+
const result = handleCustomEvent(
|
|
393
|
+
customData,
|
|
394
|
+
threadId,
|
|
395
|
+
runId,
|
|
396
|
+
subscriber,
|
|
397
|
+
manuallyEmittedState
|
|
398
|
+
);
|
|
399
|
+
manuallyEmittedState = result.manuallyEmittedState;
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
case "error": {
|
|
403
|
+
const errorData = data;
|
|
404
|
+
if (debug) {
|
|
405
|
+
console.error(
|
|
406
|
+
"[HistoryHydratingRunner] Stream error:",
|
|
407
|
+
errorData.message
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
subscriber.next({
|
|
411
|
+
type: "CUSTOM",
|
|
412
|
+
name: "on_error",
|
|
413
|
+
value: JSON.stringify(errorData),
|
|
414
|
+
rawEvent: {
|
|
415
|
+
id: runId,
|
|
416
|
+
error: errorData.error,
|
|
417
|
+
message: errorData.message
|
|
418
|
+
},
|
|
419
|
+
timestamp: Date.now(),
|
|
420
|
+
threadId,
|
|
421
|
+
runId
|
|
422
|
+
});
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
default: {
|
|
426
|
+
if (debug) {
|
|
427
|
+
console.log(
|
|
428
|
+
`[HistoryHydratingRunner] Unhandled event type: ${event}`,
|
|
429
|
+
data
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return { runId, manuallyEmittedState };
|
|
435
|
+
}
|
|
436
|
+
function handleCustomEvent(customData, threadId, runId, subscriber, manuallyEmittedState) {
|
|
437
|
+
const rawEvent = {
|
|
438
|
+
id: runId,
|
|
439
|
+
data: customData
|
|
440
|
+
};
|
|
441
|
+
const typedData = customData;
|
|
442
|
+
const eventName = typedData?.name || typedData?.event;
|
|
443
|
+
switch (eventName) {
|
|
444
|
+
case "copilotkit_manually_emit_message" /* CopilotKitManuallyEmitMessage */: {
|
|
445
|
+
const value = typedData.value;
|
|
446
|
+
const messageId = value?.message_id || runId;
|
|
447
|
+
const message = value?.message || "";
|
|
448
|
+
subscriber.next({
|
|
449
|
+
type: "TEXT_MESSAGE_START",
|
|
450
|
+
role: "assistant",
|
|
451
|
+
messageId,
|
|
452
|
+
rawEvent,
|
|
453
|
+
timestamp: Date.now(),
|
|
454
|
+
threadId,
|
|
455
|
+
runId
|
|
456
|
+
});
|
|
457
|
+
subscriber.next({
|
|
458
|
+
type: "TEXT_MESSAGE_CONTENT",
|
|
459
|
+
messageId,
|
|
460
|
+
delta: message,
|
|
461
|
+
rawEvent,
|
|
462
|
+
timestamp: Date.now(),
|
|
463
|
+
threadId,
|
|
464
|
+
runId
|
|
465
|
+
});
|
|
466
|
+
subscriber.next({
|
|
467
|
+
type: "TEXT_MESSAGE_END",
|
|
468
|
+
messageId,
|
|
469
|
+
rawEvent,
|
|
470
|
+
timestamp: Date.now(),
|
|
471
|
+
threadId,
|
|
472
|
+
runId
|
|
473
|
+
});
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
case "copilotkit_manually_emit_tool_call" /* CopilotKitManuallyEmitToolCall */: {
|
|
477
|
+
const value = typedData.value;
|
|
478
|
+
const toolCallId = value?.id || runId;
|
|
479
|
+
const toolCallName = value?.name || "";
|
|
480
|
+
const args = value?.args || {};
|
|
481
|
+
subscriber.next({
|
|
482
|
+
type: "TOOL_CALL_START",
|
|
483
|
+
toolCallId,
|
|
484
|
+
toolCallName,
|
|
485
|
+
parentMessageId: toolCallId,
|
|
486
|
+
rawEvent,
|
|
487
|
+
timestamp: Date.now(),
|
|
488
|
+
threadId,
|
|
489
|
+
runId
|
|
490
|
+
});
|
|
491
|
+
subscriber.next({
|
|
492
|
+
type: "TOOL_CALL_ARGS",
|
|
493
|
+
toolCallId,
|
|
494
|
+
delta: JSON.stringify(args),
|
|
495
|
+
rawEvent,
|
|
496
|
+
timestamp: Date.now(),
|
|
497
|
+
threadId,
|
|
498
|
+
runId
|
|
499
|
+
});
|
|
500
|
+
subscriber.next({
|
|
501
|
+
type: "TOOL_CALL_END",
|
|
502
|
+
toolCallId,
|
|
503
|
+
rawEvent,
|
|
504
|
+
timestamp: Date.now(),
|
|
505
|
+
threadId,
|
|
506
|
+
runId
|
|
507
|
+
});
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
case "copilotkit_manually_emit_intermediate_state" /* CopilotKitManuallyEmitIntermediateState */: {
|
|
511
|
+
manuallyEmittedState = typedData.value;
|
|
512
|
+
subscriber.next({
|
|
513
|
+
type: "STATE_SNAPSHOT",
|
|
514
|
+
snapshot: manuallyEmittedState,
|
|
515
|
+
rawEvent,
|
|
516
|
+
timestamp: Date.now(),
|
|
517
|
+
threadId,
|
|
518
|
+
runId
|
|
519
|
+
});
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
case "copilotkit_exit" /* CopilotKitExit */: {
|
|
523
|
+
subscriber.next({
|
|
524
|
+
type: "CUSTOM",
|
|
525
|
+
name: "Exit",
|
|
526
|
+
value: true,
|
|
527
|
+
rawEvent,
|
|
528
|
+
timestamp: Date.now(),
|
|
529
|
+
threadId,
|
|
530
|
+
runId
|
|
531
|
+
});
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
default: {
|
|
535
|
+
subscriber.next({
|
|
536
|
+
type: "CUSTOM",
|
|
537
|
+
name: eventName || "on_custom_event",
|
|
538
|
+
value: JSON.stringify(customData),
|
|
539
|
+
rawEvent,
|
|
540
|
+
timestamp: Date.now(),
|
|
541
|
+
threadId,
|
|
542
|
+
runId
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return { manuallyEmittedState };
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// src/runner/history-hydrating-runner.ts
|
|
550
|
+
var HistoryHydratingAgentRunner = class extends AgentRunner {
|
|
551
|
+
agent;
|
|
552
|
+
historyLimit;
|
|
553
|
+
debug;
|
|
554
|
+
stateExtractor;
|
|
555
|
+
activeRun = {};
|
|
556
|
+
/**
|
|
557
|
+
* Frozen agent config to prevent shared state contamination.
|
|
558
|
+
* We store the raw config values and create fresh Agent/Client instances per request.
|
|
559
|
+
* This is critical because Vercel serverless can bundle multiple routes together,
|
|
560
|
+
* causing module-level state to leak between different agent configurations.
|
|
561
|
+
*/
|
|
562
|
+
frozenConfig;
|
|
563
|
+
constructor(config) {
|
|
564
|
+
super();
|
|
565
|
+
this.agent = config.agent;
|
|
566
|
+
this.debug = config.debug ?? false;
|
|
567
|
+
this.stateExtractor = config.stateExtractor;
|
|
568
|
+
this.historyLimit = Math.min(
|
|
569
|
+
config.historyLimit ?? DEFAULT_HISTORY_LIMIT,
|
|
570
|
+
MAX_HISTORY_LIMIT
|
|
571
|
+
);
|
|
572
|
+
this.frozenConfig = Object.freeze({
|
|
573
|
+
deploymentUrl: config.deploymentUrl,
|
|
574
|
+
graphId: config.graphId,
|
|
575
|
+
langsmithApiKey: config.langsmithApiKey,
|
|
576
|
+
clientTimeoutMs: config.clientTimeoutMs ?? DEFAULT_TIMEOUT,
|
|
577
|
+
onRequest: config.onRequest
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Creates a fresh LangGraphAgent instance using the frozen config.
|
|
582
|
+
* Uses our isolated agent creator to prevent shared state contamination.
|
|
583
|
+
*/
|
|
584
|
+
createFreshAgent() {
|
|
585
|
+
return createIsolatedAgent({
|
|
586
|
+
deploymentUrl: this.frozenConfig.deploymentUrl,
|
|
587
|
+
graphId: this.frozenConfig.graphId,
|
|
588
|
+
langsmithApiKey: this.frozenConfig.langsmithApiKey,
|
|
589
|
+
clientTimeoutMs: this.frozenConfig.clientTimeoutMs,
|
|
590
|
+
onRequest: this.frozenConfig.onRequest
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Creates a fresh LangGraph Client instance using the frozen config.
|
|
595
|
+
* This prevents shared state contamination in serverless environments.
|
|
596
|
+
*/
|
|
597
|
+
createFreshClient() {
|
|
598
|
+
return new Client({
|
|
599
|
+
apiUrl: this.frozenConfig.deploymentUrl,
|
|
600
|
+
apiKey: this.frozenConfig.langsmithApiKey,
|
|
601
|
+
timeoutMs: this.frozenConfig.clientTimeoutMs,
|
|
602
|
+
onRequest: this.frozenConfig.onRequest
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Log a message if debug mode is enabled.
|
|
607
|
+
*/
|
|
608
|
+
log(message, ...args) {
|
|
609
|
+
if (this.debug) {
|
|
610
|
+
console.log(`[HistoryHydratingRunner] ${message}`, ...args);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Log a warning.
|
|
615
|
+
*/
|
|
616
|
+
warn(message, ...args) {
|
|
617
|
+
console.warn(`[HistoryHydratingRunner] ${message}`, ...args);
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Log an error.
|
|
621
|
+
*/
|
|
622
|
+
error(message, ...args) {
|
|
623
|
+
console.error(`[HistoryHydratingRunner] ${message}`, ...args);
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Run the agent with a FRESH agent instance.
|
|
627
|
+
* CRITICAL: We cannot trust request.agent (cloned by CopilotKit) because
|
|
628
|
+
* its internal Client may have been corrupted by shared module state in
|
|
629
|
+
* Vercel serverless environments. Create a completely fresh agent with
|
|
630
|
+
* our frozen config to guarantee the correct deployment URL is used.
|
|
631
|
+
*/
|
|
632
|
+
run(request) {
|
|
633
|
+
const freshAgent = this.createFreshAgent();
|
|
634
|
+
const inputWithProps = request.input;
|
|
635
|
+
const forwardedProps = inputWithProps.forwardedProps;
|
|
636
|
+
const existingState = request.input.state || {};
|
|
637
|
+
let enrichedState;
|
|
638
|
+
if (this.stateExtractor) {
|
|
639
|
+
const extractedState = this.stateExtractor(request.input, forwardedProps);
|
|
640
|
+
enrichedState = {
|
|
641
|
+
...existingState,
|
|
642
|
+
...extractedState
|
|
643
|
+
};
|
|
644
|
+
} else {
|
|
645
|
+
enrichedState = existingState;
|
|
646
|
+
}
|
|
647
|
+
this.log("State extraction:", {
|
|
648
|
+
hasStateExtractor: !!this.stateExtractor,
|
|
649
|
+
hasForwardedProps: !!forwardedProps,
|
|
650
|
+
hasState: !!request.input.state,
|
|
651
|
+
threadId: request.input.threadId
|
|
652
|
+
});
|
|
653
|
+
freshAgent.setState(enrichedState);
|
|
654
|
+
const inputWithState = {
|
|
655
|
+
...request.input,
|
|
656
|
+
state: enrichedState
|
|
657
|
+
};
|
|
658
|
+
return freshAgent.run(inputWithState);
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Delegate isRunning to the agent.
|
|
662
|
+
*/
|
|
663
|
+
async isRunning() {
|
|
664
|
+
return this.agent.isRunning;
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Delegate stop to the agent.
|
|
668
|
+
*/
|
|
669
|
+
async stop(_request) {
|
|
670
|
+
const result = this.agent.abortRun();
|
|
671
|
+
return result !== void 0 ? result : true;
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Override connect to add history hydration support.
|
|
675
|
+
*
|
|
676
|
+
* When reconnecting to a thread:
|
|
677
|
+
* 1. Fetches ALL thread history (checkpoints) from LangGraph
|
|
678
|
+
* 2. Extracts and deduplicates messages from all checkpoints
|
|
679
|
+
* 3. Transforms historical messages to CopilotKit format
|
|
680
|
+
* 4. Emits MESSAGES_SNAPSHOT and STATE_SNAPSHOT events
|
|
681
|
+
* 5. Completes the observable
|
|
682
|
+
*/
|
|
683
|
+
connect(request) {
|
|
684
|
+
const { threadId } = request;
|
|
685
|
+
const client = this.createFreshClient();
|
|
686
|
+
return new Observable((subscriber) => {
|
|
687
|
+
const hydrate = async () => {
|
|
688
|
+
try {
|
|
689
|
+
const history = await client.threads.getHistory(threadId, {
|
|
690
|
+
limit: this.historyLimit > 0 ? this.historyLimit : DEFAULT_HISTORY_LIMIT
|
|
691
|
+
});
|
|
692
|
+
if (!history || history.length === 0) {
|
|
693
|
+
this.warn(`No history found for thread ${threadId}`);
|
|
694
|
+
const fallbackRunId = "hydration_" + Math.random().toString(36).slice(2);
|
|
695
|
+
subscriber.next({
|
|
696
|
+
type: EventType.RUN_STARTED,
|
|
697
|
+
timestamp: Date.now(),
|
|
698
|
+
threadId,
|
|
699
|
+
runId: fallbackRunId
|
|
700
|
+
});
|
|
701
|
+
subscriber.next({
|
|
702
|
+
type: EventType.MESSAGES_SNAPSHOT,
|
|
703
|
+
messages: [],
|
|
704
|
+
timestamp: Date.now(),
|
|
705
|
+
threadId,
|
|
706
|
+
runId: fallbackRunId
|
|
707
|
+
});
|
|
708
|
+
subscriber.next({
|
|
709
|
+
type: EventType.RUN_FINISHED,
|
|
710
|
+
timestamp: Date.now(),
|
|
711
|
+
threadId,
|
|
712
|
+
runId: fallbackRunId
|
|
713
|
+
});
|
|
714
|
+
subscriber.complete();
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const allMessages = [];
|
|
718
|
+
const seenMessageIds = /* @__PURE__ */ new Set();
|
|
719
|
+
for (const checkpoint of history.reverse()) {
|
|
720
|
+
const state = checkpoint;
|
|
721
|
+
if (state.values?.messages) {
|
|
722
|
+
const messages = state.values.messages || [];
|
|
723
|
+
for (const msg of messages) {
|
|
724
|
+
if (!seenMessageIds.has(msg.id)) {
|
|
725
|
+
seenMessageIds.add(msg.id);
|
|
726
|
+
allMessages.push(msg);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
this.log(
|
|
732
|
+
`Loaded ${allMessages.length} unique messages from ${history.length} checkpoints`
|
|
733
|
+
);
|
|
734
|
+
const limitedMessages = this.historyLimit > 0 ? allMessages.slice(-this.historyLimit) : allMessages;
|
|
735
|
+
const transformedMessages = transformMessages(limitedMessages, {
|
|
736
|
+
debug: this.debug
|
|
737
|
+
});
|
|
738
|
+
let runId;
|
|
739
|
+
try {
|
|
740
|
+
const runs = await client.runs.list(threadId);
|
|
741
|
+
runId = runs && runs.length > 0 ? runs[0].run_id : "hydration_" + Math.random().toString(36).slice(2);
|
|
742
|
+
} catch (error) {
|
|
743
|
+
this.warn("Failed to fetch runs, using generated ID:", error);
|
|
744
|
+
runId = "hydration_" + Math.random().toString(36).slice(2);
|
|
745
|
+
}
|
|
746
|
+
subscriber.next({
|
|
747
|
+
type: EventType.RUN_STARTED,
|
|
748
|
+
timestamp: Date.now(),
|
|
749
|
+
threadId,
|
|
750
|
+
runId
|
|
751
|
+
});
|
|
752
|
+
subscriber.next({
|
|
753
|
+
type: EventType.MESSAGES_SNAPSHOT,
|
|
754
|
+
messages: transformedMessages,
|
|
755
|
+
timestamp: Date.now(),
|
|
756
|
+
threadId,
|
|
757
|
+
runId
|
|
758
|
+
});
|
|
759
|
+
const latestState = history[history.length - 1];
|
|
760
|
+
if (latestState.values) {
|
|
761
|
+
subscriber.next({
|
|
762
|
+
type: "STATE_SNAPSHOT",
|
|
763
|
+
snapshot: latestState.values,
|
|
764
|
+
rawEvent: {
|
|
765
|
+
id: runId,
|
|
766
|
+
event: "values",
|
|
767
|
+
data: latestState.values
|
|
768
|
+
},
|
|
769
|
+
timestamp: Date.now(),
|
|
770
|
+
threadId,
|
|
771
|
+
runId
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
const interruptedTask = latestState.tasks?.find(
|
|
775
|
+
(task) => task.interrupts && task.interrupts.length > 0
|
|
776
|
+
);
|
|
777
|
+
if (interruptedTask && interruptedTask.interrupts && interruptedTask.interrupts.length > 0) {
|
|
778
|
+
const interrupt = interruptedTask.interrupts[0];
|
|
779
|
+
const interruptValue = interrupt?.value;
|
|
780
|
+
subscriber.next({
|
|
781
|
+
type: "CUSTOM",
|
|
782
|
+
name: "on_interrupt",
|
|
783
|
+
value: JSON.stringify(interruptValue),
|
|
784
|
+
rawEvent: {
|
|
785
|
+
id: runId,
|
|
786
|
+
value: interruptValue
|
|
787
|
+
},
|
|
788
|
+
timestamp: Date.now(),
|
|
789
|
+
threadId,
|
|
790
|
+
runId
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
const isThreadBusy = latestState.next && latestState.next.length > 0;
|
|
794
|
+
let activeRun;
|
|
795
|
+
if (isThreadBusy) {
|
|
796
|
+
try {
|
|
797
|
+
const runs = await client.runs.list(threadId);
|
|
798
|
+
activeRun = runs?.find(
|
|
799
|
+
(run) => run.status === "running" || run.status === "pending"
|
|
800
|
+
);
|
|
801
|
+
} catch (error) {
|
|
802
|
+
this.warn("Failed to check for active runs:", error);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
if (activeRun) {
|
|
806
|
+
this.log(`Joining active stream for run ${activeRun.run_id}`);
|
|
807
|
+
try {
|
|
808
|
+
await this.joinAndProcessStream(
|
|
809
|
+
client,
|
|
810
|
+
threadId,
|
|
811
|
+
activeRun.run_id,
|
|
812
|
+
subscriber
|
|
813
|
+
);
|
|
814
|
+
} catch (error) {
|
|
815
|
+
this.error("Error joining stream:", error);
|
|
816
|
+
}
|
|
817
|
+
} else {
|
|
818
|
+
subscriber.next({
|
|
819
|
+
type: EventType.RUN_FINISHED,
|
|
820
|
+
timestamp: Date.now(),
|
|
821
|
+
threadId,
|
|
822
|
+
runId
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
subscriber.complete();
|
|
826
|
+
} catch (error) {
|
|
827
|
+
this.error("Failed to hydrate history:", error);
|
|
828
|
+
const fallbackRunId = "hydration_error_" + Math.random().toString(36).slice(2);
|
|
829
|
+
subscriber.next({
|
|
830
|
+
type: EventType.RUN_STARTED,
|
|
831
|
+
timestamp: Date.now(),
|
|
832
|
+
threadId,
|
|
833
|
+
runId: fallbackRunId
|
|
834
|
+
});
|
|
835
|
+
subscriber.next({
|
|
836
|
+
type: EventType.MESSAGES_SNAPSHOT,
|
|
837
|
+
messages: [],
|
|
838
|
+
timestamp: Date.now(),
|
|
839
|
+
threadId,
|
|
840
|
+
runId: fallbackRunId
|
|
841
|
+
});
|
|
842
|
+
subscriber.next({
|
|
843
|
+
type: EventType.RUN_FINISHED,
|
|
844
|
+
timestamp: Date.now(),
|
|
845
|
+
threadId,
|
|
846
|
+
runId: fallbackRunId
|
|
847
|
+
});
|
|
848
|
+
subscriber.complete();
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
hydrate();
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Joins an active stream and processes its events.
|
|
856
|
+
*
|
|
857
|
+
* This method connects to an already-running LangGraph execution and
|
|
858
|
+
* processes all incoming events, transforming them to BaseEvent format.
|
|
859
|
+
*
|
|
860
|
+
* Tracks started messages and tool calls to handle mid-stream joins where
|
|
861
|
+
* we might receive CONTENT/END events without having seen START events.
|
|
862
|
+
*/
|
|
863
|
+
async joinAndProcessStream(client, threadId, runId, subscriber) {
|
|
864
|
+
const startedMessages = /* @__PURE__ */ new Set();
|
|
865
|
+
const startedToolCalls = /* @__PURE__ */ new Set();
|
|
866
|
+
try {
|
|
867
|
+
const stream = client.runs.joinStream(threadId, runId, {
|
|
868
|
+
streamMode: ["events", "values", "updates", "custom"]
|
|
869
|
+
});
|
|
870
|
+
let currentRunId = runId;
|
|
871
|
+
let manuallyEmittedState = this.activeRun.manuallyEmittedState;
|
|
872
|
+
for await (const chunk of stream) {
|
|
873
|
+
try {
|
|
874
|
+
const result = await processStreamChunk(chunk, {
|
|
875
|
+
threadId,
|
|
876
|
+
runId: currentRunId,
|
|
877
|
+
subscriber,
|
|
878
|
+
startedMessages,
|
|
879
|
+
startedToolCalls,
|
|
880
|
+
debug: this.debug,
|
|
881
|
+
manuallyEmittedState
|
|
882
|
+
});
|
|
883
|
+
currentRunId = result.runId;
|
|
884
|
+
manuallyEmittedState = result.manuallyEmittedState;
|
|
885
|
+
} catch (chunkError) {
|
|
886
|
+
this.error("Error processing stream chunk:", chunkError);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
this.activeRun.manuallyEmittedState = manuallyEmittedState;
|
|
890
|
+
try {
|
|
891
|
+
const state = await client.threads.getState(threadId);
|
|
892
|
+
const threadState = state;
|
|
893
|
+
const interruptedTask = threadState.tasks?.find(
|
|
894
|
+
(task) => task.interrupts && task.interrupts.length > 0
|
|
895
|
+
);
|
|
896
|
+
if (interruptedTask && interruptedTask.interrupts && interruptedTask.interrupts.length > 0) {
|
|
897
|
+
const interrupt = interruptedTask.interrupts[0];
|
|
898
|
+
const interruptValue = interrupt?.value;
|
|
899
|
+
subscriber.next({
|
|
900
|
+
type: "CUSTOM",
|
|
901
|
+
name: "on_interrupt",
|
|
902
|
+
value: JSON.stringify(interruptValue),
|
|
903
|
+
rawEvent: {
|
|
904
|
+
id: currentRunId,
|
|
905
|
+
value: interruptValue
|
|
906
|
+
},
|
|
907
|
+
timestamp: Date.now(),
|
|
908
|
+
threadId,
|
|
909
|
+
runId: currentRunId
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
} catch (stateError) {
|
|
913
|
+
this.warn("Failed to check for interrupts after stream:", stateError);
|
|
914
|
+
}
|
|
915
|
+
subscriber.next({
|
|
916
|
+
type: EventType.RUN_FINISHED,
|
|
917
|
+
timestamp: Date.now(),
|
|
918
|
+
threadId,
|
|
919
|
+
runId: currentRunId
|
|
920
|
+
});
|
|
921
|
+
} catch (error) {
|
|
922
|
+
this.error("Error in joinAndProcessStream:", error);
|
|
923
|
+
subscriber.next({
|
|
924
|
+
type: EventType.RUN_FINISHED,
|
|
925
|
+
timestamp: Date.now(),
|
|
926
|
+
threadId,
|
|
927
|
+
runId
|
|
928
|
+
});
|
|
929
|
+
throw error;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
export { CustomEventNames, DEFAULT_HISTORY_LIMIT, DEFAULT_TIMEOUT, HistoryHydratingAgentRunner, LangGraphEventTypes, MAX_HISTORY_LIMIT, createIsolatedAgent, extractContent, processStreamChunk, transformMessages };
|
|
935
|
+
//# sourceMappingURL=index.js.map
|
|
936
|
+
//# sourceMappingURL=index.js.map
|