@copilotkit/sdk-js 1.78.0 → 1.79.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/langgraph/middleware.cjs +40 -54
- package/dist/langgraph/middleware.cjs.map +1 -1
- package/dist/langgraph/middleware.d.cts.map +1 -1
- package/dist/langgraph/middleware.d.mts.map +1 -1
- package/dist/langgraph/middleware.mjs +41 -55
- package/dist/langgraph/middleware.mjs.map +1 -1
- package/package.json +2 -2
- package/src/langgraph/__tests__/middleware.properties.test.ts +88 -19
- package/src/langgraph/__tests__/middleware.test.ts +191 -44
- package/src/langgraph/middleware.ts +97 -89
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createMiddleware, AIMessage
|
|
1
|
+
import { createMiddleware, AIMessage } from "langchain";
|
|
2
2
|
import type { InteropZodObject } from "@langchain/core/utils/types";
|
|
3
3
|
import type {
|
|
4
4
|
StandardJSONSchemaV1,
|
|
@@ -258,13 +258,7 @@ const buildStateNote = (
|
|
|
258
258
|
return `Current agent state:\n${body}`;
|
|
259
259
|
};
|
|
260
260
|
|
|
261
|
-
const
|
|
262
|
-
const note = buildStateNote(
|
|
263
|
-
(request.state ?? {}) as Record<string, unknown>,
|
|
264
|
-
expose,
|
|
265
|
-
);
|
|
266
|
-
if (!note) return request;
|
|
267
|
-
|
|
261
|
+
const appendSystemNote = (request: any, note: string): any => {
|
|
268
262
|
const existingMessage = request.systemMessage;
|
|
269
263
|
if (existingMessage != null) {
|
|
270
264
|
const separator = existingMessage.text === "" ? "" : "\n\n";
|
|
@@ -291,100 +285,114 @@ const applyStateNote = (request: any, expose: ExposeStateOption): any => {
|
|
|
291
285
|
};
|
|
292
286
|
};
|
|
293
287
|
|
|
294
|
-
const
|
|
295
|
-
const
|
|
288
|
+
const applyStateNote = (request: any, expose: ExposeStateOption): any => {
|
|
289
|
+
const note = buildStateNote(
|
|
290
|
+
(request.state ?? {}) as Record<string, unknown>,
|
|
291
|
+
expose,
|
|
292
|
+
);
|
|
293
|
+
return note ? appendSystemNote(request, note) : request;
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const APP_CONTEXT_PREFIX = "App Context:\n";
|
|
297
|
+
|
|
298
|
+
// LangGraph runtime context carries trusted run configuration (thread, tenant
|
|
299
|
+
// and user ids) that must never reach the model, so only a CopilotKit carrier
|
|
300
|
+
// inside it counts: an "ag-ui" or "copilotkit" namespace, or a legacy
|
|
301
|
+
// unnamespaced carrier with an actions or context key. See #7077.
|
|
302
|
+
const runtimeCarrierProperties = (
|
|
303
|
+
carrier: unknown,
|
|
304
|
+
namespace: "ag-ui" | "copilotkit",
|
|
305
|
+
): Record<string, unknown> => {
|
|
306
|
+
if (!isPropertyBag(carrier)) return {};
|
|
307
|
+
const nested = carrier[namespace];
|
|
308
|
+
if (isPropertyBag(nested) && Object.keys(nested).length > 0) return nested;
|
|
309
|
+
if (
|
|
310
|
+
namespace === "copilotkit" &&
|
|
311
|
+
("actions" in carrier || "context" in carrier)
|
|
312
|
+
)
|
|
313
|
+
return carrier;
|
|
314
|
+
return {};
|
|
315
|
+
};
|
|
296
316
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
317
|
+
// Interception bookkeeping alone does not make a state namespace present.
|
|
318
|
+
const BOOKKEEPING_KEYS = new Set([
|
|
319
|
+
"interceptedToolCalls",
|
|
320
|
+
"originalAIMessageId",
|
|
321
|
+
]);
|
|
300
322
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
323
|
+
const definedProperties = (value: unknown): Record<string, unknown> =>
|
|
324
|
+
isPropertyBag(value)
|
|
325
|
+
? Object.fromEntries(
|
|
326
|
+
Object.entries(value).filter(([, entry]) => entry !== undefined),
|
|
327
|
+
)
|
|
328
|
+
: {};
|
|
329
|
+
|
|
330
|
+
// Each namespace comes from state, or from its runtime carrier when state has
|
|
331
|
+
// none of it. CopilotKit then merges over AG-UI, as in the Python middleware.
|
|
332
|
+
const contextNamespace = (
|
|
333
|
+
state: unknown,
|
|
334
|
+
runtimeContext: unknown,
|
|
335
|
+
namespace: "ag-ui" | "copilotkit",
|
|
336
|
+
): Record<string, unknown> => {
|
|
337
|
+
const fromState = definedProperties(
|
|
338
|
+
isPropertyBag(state) ? state[namespace] : undefined,
|
|
339
|
+
);
|
|
340
|
+
return Object.keys(fromState).some((key) => !BOOKKEEPING_KEYS.has(key))
|
|
341
|
+
? fromState
|
|
342
|
+
: definedProperties(runtimeCarrierProperties(runtimeContext, namespace));
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const buildAppContextNote = (
|
|
346
|
+
state: unknown,
|
|
347
|
+
runtimeContext: unknown,
|
|
348
|
+
): string | null => {
|
|
349
|
+
const properties = mergeProperties(
|
|
350
|
+
contextNamespace(state, runtimeContext, "ag-ui"),
|
|
351
|
+
contextNamespace(state, runtimeContext, "copilotkit"),
|
|
352
|
+
);
|
|
353
|
+
// Forwarded request headers are transport data, never model context.
|
|
354
|
+
const appContext = isPropertyBag(properties.context)
|
|
355
|
+
? Object.fromEntries(
|
|
356
|
+
Object.entries(properties.context).filter(
|
|
357
|
+
([key]) => key !== "copilotkit_forwarded_headers",
|
|
358
|
+
),
|
|
359
|
+
)
|
|
360
|
+
: properties.context;
|
|
306
361
|
|
|
307
|
-
// Check if appContext is missing or empty
|
|
308
362
|
const isEmptyContext =
|
|
309
363
|
!appContext ||
|
|
310
364
|
(typeof appContext === "string" && appContext.trim() === "") ||
|
|
311
365
|
(typeof appContext === "object" && Object.keys(appContext).length === 0);
|
|
366
|
+
if (isEmptyContext) return null;
|
|
312
367
|
|
|
313
|
-
if (isEmptyContext) {
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Create the context content
|
|
318
368
|
const contextContent =
|
|
319
369
|
typeof appContext === "string"
|
|
320
370
|
? appContext
|
|
321
371
|
: JSON.stringify(appContext, null, 2);
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
// Helper to get message content as string
|
|
326
|
-
const getContentString = (msg: any): string | null => {
|
|
327
|
-
if (typeof msg.content === "string") return msg.content;
|
|
328
|
-
if (Array.isArray(msg.content) && msg.content[0]?.text)
|
|
329
|
-
return msg.content[0].text;
|
|
330
|
-
return null;
|
|
331
|
-
};
|
|
332
|
-
|
|
333
|
-
// Find the first system/developer message (not our context message) to determine
|
|
334
|
-
// where to insert our context message (right after it)
|
|
335
|
-
let firstSystemIndex = -1;
|
|
336
|
-
|
|
337
|
-
for (let i = 0; i < messages.length; i++) {
|
|
338
|
-
const msg = messages[i];
|
|
339
|
-
const type = msg._getType?.();
|
|
340
|
-
if (type === "system" || type === "developer") {
|
|
341
|
-
const content = getContentString(msg);
|
|
342
|
-
// Skip if this is our own context message
|
|
343
|
-
if (content?.startsWith(contextMessagePrefix)) {
|
|
344
|
-
continue;
|
|
345
|
-
}
|
|
346
|
-
firstSystemIndex = i;
|
|
347
|
-
break;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
372
|
+
return `${APP_CONTEXT_PREFIX}${contextContent}`;
|
|
373
|
+
};
|
|
350
374
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
if (content?.startsWith(contextMessagePrefix)) {
|
|
359
|
-
existingContextIndex = i;
|
|
360
|
-
break;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
}
|
|
375
|
+
const isAppContextMessage = (msg: any): boolean => {
|
|
376
|
+
const type = msg?._getType?.();
|
|
377
|
+
if (type !== "system" && type !== "developer") return false;
|
|
378
|
+
const content =
|
|
379
|
+
typeof msg.content === "string" ? msg.content : msg.content?.[0]?.text;
|
|
380
|
+
return typeof content === "string" && content.startsWith(APP_CONTEXT_PREFIX);
|
|
381
|
+
};
|
|
364
382
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
} else {
|
|
375
|
-
// Insert after the first system message, or at position 0 if no system message
|
|
376
|
-
const insertIndex = firstSystemIndex !== -1 ? firstSystemIndex + 1 : 0;
|
|
377
|
-
updatedMessages = [
|
|
378
|
-
...messages.slice(0, insertIndex),
|
|
379
|
-
contextMessage,
|
|
380
|
-
...messages.slice(insertIndex),
|
|
381
|
-
];
|
|
383
|
+
// App context goes into the one leading system message, never a message of its
|
|
384
|
+
// own: Anthropic and Gemini reject a second system message. Older releases
|
|
385
|
+
// wrote an "App Context:" SystemMessage into the thread, so drop any such
|
|
386
|
+
// message from the model's input too.
|
|
387
|
+
const applyAppContextNote = (request: any): any => {
|
|
388
|
+
const messages = request.messages ?? [];
|
|
389
|
+
const keptMessages = messages.filter((msg: any) => !isAppContextMessage(msg));
|
|
390
|
+
if (keptMessages.length !== messages.length) {
|
|
391
|
+
request = { ...request, messages: keptMessages };
|
|
382
392
|
}
|
|
383
393
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
messages: updatedMessages,
|
|
387
|
-
};
|
|
394
|
+
const note = buildAppContextNote(request.state, request.runtime?.context);
|
|
395
|
+
return note ? appendSystemNote(request, note) : request;
|
|
388
396
|
};
|
|
389
397
|
|
|
390
398
|
/**
|
|
@@ -478,9 +486,11 @@ const buildMiddlewareInput = (
|
|
|
478
486
|
|
|
479
487
|
stateSchema: copilotKitStateSchema as unknown as InteropZodObject,
|
|
480
488
|
|
|
481
|
-
// Inject frontend tools, surface user state, and forward
|
|
489
|
+
// Inject frontend tools, surface user state and app context, and forward
|
|
490
|
+
// x-aimock-* headers
|
|
482
491
|
wrapModelCall: async (request: any, handler: (req: any) => Promise<any>) => {
|
|
483
492
|
request = applyStateNote(request, exposeState);
|
|
493
|
+
request = applyAppContextNote(request);
|
|
484
494
|
|
|
485
495
|
// Forward x-aimock-* headers from the incoming AG-UI request
|
|
486
496
|
const forwardedHeaders = getForwardedHeaders();
|
|
@@ -605,8 +615,6 @@ const buildMiddlewareInput = (
|
|
|
605
615
|
return handler(request);
|
|
606
616
|
},
|
|
607
617
|
|
|
608
|
-
beforeAgent: createAppContextBeforeAgent,
|
|
609
|
-
|
|
610
618
|
// Restore frontend tool calls to AIMessage before agent exits
|
|
611
619
|
afterAgent: (state) => {
|
|
612
620
|
// Drop the bridged A2UI tool for this run — all tool calls for the turn
|