@copilotkit/sdk-js 1.78.0 → 1.79.1-canary.1792034358

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.
@@ -1,4 +1,4 @@
1
- import { createMiddleware, AIMessage, SystemMessage } from "langchain";
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 applyStateNote = (request: any, expose: ExposeStateOption): any => {
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 createAppContextBeforeAgent = (state, runtime) => {
295
- const messages = state.messages;
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
- if (!messages || messages.length === 0) {
298
- return;
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
- // Get app context from runtime
302
- const properties = effectiveProperties(state);
303
- const appContext = Object.prototype.hasOwnProperty.call(properties, "context")
304
- ? properties.context
305
- : runtime?.context;
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
- const contextMessageContent = `App Context:\n${contextContent}`;
323
- const contextMessagePrefix = "App Context:\n";
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
- // Check if our context message already exists
352
- let existingContextIndex = -1;
353
- for (let i = 0; i < messages.length; i++) {
354
- const msg = messages[i];
355
- const type = msg._getType?.();
356
- if (type === "system" || type === "developer") {
357
- const content = getContentString(msg);
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
- // Create the context message
366
- const contextMessage = new SystemMessage({ content: contextMessageContent });
367
-
368
- let updatedMessages;
369
-
370
- if (existingContextIndex !== -1) {
371
- // Replace existing context message
372
- updatedMessages = [...messages];
373
- updatedMessages[existingContextIndex] = contextMessage;
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
- return {
385
- ...state,
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 x-aimock-* headers
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