@copilotkit/channels-core 0.4.1-canary.perfall1 → 0.5.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 (47) hide show
  1. package/README.md +7 -8
  2. package/dist/canonical-run-loop.test.d.ts +2 -0
  3. package/dist/canonical-run-loop.test.d.ts.map +1 -0
  4. package/dist/canonical-run-loop.test.js +453 -0
  5. package/dist/codec.d.ts +2 -3
  6. package/dist/codec.d.ts.map +1 -1
  7. package/dist/create-channel.d.ts +110 -4
  8. package/dist/create-channel.d.ts.map +1 -1
  9. package/dist/create-channel.js +258 -92
  10. package/dist/create-channel.test.js +552 -29
  11. package/dist/delivery-error.d.ts +17 -0
  12. package/dist/delivery-error.d.ts.map +1 -0
  13. package/dist/delivery-error.js +22 -0
  14. package/dist/index.d.ts +6 -2
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +8 -0
  17. package/dist/managed-v1-await-choice-guard.test.d.ts +2 -0
  18. package/dist/managed-v1-await-choice-guard.test.d.ts.map +1 -0
  19. package/dist/managed-v1-await-choice-guard.test.js +52 -0
  20. package/dist/platform-adapter.d.ts +74 -11
  21. package/dist/platform-adapter.d.ts.map +1 -1
  22. package/dist/run-loop.d.ts +25 -6
  23. package/dist/run-loop.d.ts.map +1 -1
  24. package/dist/run-loop.js +289 -39
  25. package/dist/run-loop.test.js +37 -0
  26. package/dist/sanitize-agent-events.d.ts +24 -0
  27. package/dist/sanitize-agent-events.d.ts.map +1 -0
  28. package/dist/sanitize-agent-events.js +88 -0
  29. package/dist/sanitize-agent-events.test.d.ts +2 -0
  30. package/dist/sanitize-agent-events.test.d.ts.map +1 -0
  31. package/dist/sanitize-agent-events.test.js +194 -0
  32. package/dist/source-platform.test.d.ts +2 -0
  33. package/dist/source-platform.test.d.ts.map +1 -0
  34. package/dist/source-platform.test.js +149 -0
  35. package/dist/testing/fake-adapter.d.ts +8 -1
  36. package/dist/testing/fake-adapter.d.ts.map +1 -1
  37. package/dist/testing/fake-adapter.js +30 -1
  38. package/dist/testing/fake-agent.d.ts +5 -0
  39. package/dist/testing/fake-agent.d.ts.map +1 -1
  40. package/dist/testing/fake-agent.js +12 -0
  41. package/dist/thread-promise-contract.test.d.ts +2 -0
  42. package/dist/thread-promise-contract.test.d.ts.map +1 -0
  43. package/dist/thread-promise-contract.test.js +37 -0
  44. package/dist/thread.d.ts +5 -0
  45. package/dist/thread.d.ts.map +1 -1
  46. package/dist/thread.js +338 -243
  47. package/package.json +4 -4
package/dist/run-loop.js CHANGED
@@ -1,4 +1,179 @@
1
+ import { EventType } from "@ag-ui/client";
1
2
  import { parseToolArgs, stringifyHandlerResult } from "./tools.js";
3
+ import { isChannelDeliveryTerminatedError } from "./delivery-error.js";
4
+ /**
5
+ * Merge two subscriber callback results while preserving either subscriber's
6
+ * request to stop propagation.
7
+ */
8
+ function mergeSubscriberResults(firstResult, secondResult) {
9
+ if ((firstResult === undefined || firstResult === null) &&
10
+ (secondResult === undefined || secondResult === null)) {
11
+ return undefined;
12
+ }
13
+ return {
14
+ ...(typeof firstResult === "object" ? firstResult : {}),
15
+ ...(typeof secondResult === "object" ? secondResult : {}),
16
+ stopPropagation: (typeof firstResult === "object" &&
17
+ firstResult !== null &&
18
+ "stopPropagation" in firstResult &&
19
+ firstResult.stopPropagation === true) ||
20
+ (typeof secondResult === "object" &&
21
+ secondResult !== null &&
22
+ "stopPropagation" in secondResult &&
23
+ secondResult.stopPropagation === true),
24
+ };
25
+ }
26
+ /**
27
+ * Invoke canonical ingestion before rendering, but always invoke both. A
28
+ * renderer failure therefore cannot prevent the same event from reaching the
29
+ * runner.
30
+ */
31
+ async function invokeSubscriberPair(rendererCallback, ingestionCallback, params, onRendererError, isRendererClosed) {
32
+ let rendererResult;
33
+ let ingestionResult;
34
+ let rendererError;
35
+ let ingestionError;
36
+ let rendererFailed = false;
37
+ let ingestionFailed = false;
38
+ try {
39
+ ingestionResult = await ingestionCallback?.(params);
40
+ }
41
+ catch (error) {
42
+ ingestionFailed = true;
43
+ ingestionError = error;
44
+ }
45
+ if (!isRendererClosed?.()) {
46
+ try {
47
+ rendererResult = await rendererCallback?.(params);
48
+ }
49
+ catch (error) {
50
+ rendererFailed = true;
51
+ rendererError = error;
52
+ }
53
+ }
54
+ if (ingestionFailed)
55
+ throw ingestionError;
56
+ if (rendererFailed) {
57
+ if (!onRendererError)
58
+ throw rendererError;
59
+ onRendererError(rendererError);
60
+ }
61
+ return mergeSubscriberResults(rendererResult, ingestionResult);
62
+ }
63
+ /**
64
+ * Return one stamped event object for every callback generated from the same
65
+ * raw AG-UI event.
66
+ */
67
+ function canonicalizeSubscriberParams(params, options, stampedEvents) {
68
+ if (typeof params !== "object" ||
69
+ params === null ||
70
+ !("event" in params) ||
71
+ typeof params.event !== "object" ||
72
+ params.event === null ||
73
+ !("type" in params.event)) {
74
+ return params;
75
+ }
76
+ const event = params.event;
77
+ if (options.canonicalRun &&
78
+ (event.type === EventType.RUN_STARTED ||
79
+ event.type === EventType.RUN_FINISHED ||
80
+ event.type === EventType.RUN_ERROR)) {
81
+ if (event.type === EventType.RUN_ERROR) {
82
+ options.onInnerRunError?.(event);
83
+ }
84
+ return undefined;
85
+ }
86
+ if (!options.canonicalRun)
87
+ return params;
88
+ let stamped = stampedEvents.get(event);
89
+ if (!stamped) {
90
+ stamped = {
91
+ ...event,
92
+ threadId: options.canonicalRun.threadId,
93
+ runId: options.canonicalRun.runId,
94
+ };
95
+ stampedEvents.set(event, stamped);
96
+ }
97
+ return { ...params, event: stamped };
98
+ }
99
+ /**
100
+ * Fan one agent event stream out to both the provider renderer and a canonical
101
+ * runner subscriber. The proxy covers every current and future subscriber
102
+ * callback without keeping a second callback-name registry in Channels.
103
+ */
104
+ export function mergeAgentSubscribers(first, second, options = {}) {
105
+ const stampedEvents = new WeakMap();
106
+ return new Proxy({}, {
107
+ get(_target, property) {
108
+ const firstCallback = first[property];
109
+ const secondCallback = second[property];
110
+ if (typeof firstCallback !== "function" &&
111
+ typeof secondCallback !== "function") {
112
+ return undefined;
113
+ }
114
+ return async (params) => {
115
+ const canonicalParams = canonicalizeSubscriberParams(params, options, stampedEvents);
116
+ if (canonicalParams === undefined)
117
+ return undefined;
118
+ return invokeSubscriberPair(typeof firstCallback === "function"
119
+ ? firstCallback
120
+ : undefined, typeof secondCallback === "function"
121
+ ? secondCallback
122
+ : undefined, canonicalParams, options.onRendererError, options.isRendererClosed);
123
+ };
124
+ },
125
+ });
126
+ }
127
+ /** Convert an inner AG-UI error event into the error rejected by the outer run. */
128
+ function errorFromInnerRun(event) {
129
+ const error = new Error(event.message);
130
+ error.name = "ChannelInnerRunError";
131
+ if (event.code)
132
+ error.code = event.code;
133
+ return error;
134
+ }
135
+ /**
136
+ * Emit one canonical lifecycle event through the same ingestion-first fanout
137
+ * used by streamed events.
138
+ */
139
+ async function emitCanonicalLifecycleEvent(event, result, args, onRendererError, isRendererClosed) {
140
+ const canonicalRun = args.canonicalRun;
141
+ if (!canonicalRun)
142
+ return;
143
+ const input = {
144
+ threadId: canonicalRun.threadId,
145
+ runId: canonicalRun.runId,
146
+ messages: args.agent.messages,
147
+ state: args.agent.state,
148
+ tools: [...args.toolDescriptors],
149
+ context: [...args.context],
150
+ forwardedProps: {},
151
+ };
152
+ const baseParams = {
153
+ messages: args.agent.messages,
154
+ state: args.agent.state,
155
+ agent: args.agent,
156
+ input,
157
+ };
158
+ const ingestion = args.subscriber ?? {};
159
+ const renderer = args.renderer.subscriber;
160
+ const eventParams = { ...baseParams, event };
161
+ await invokeSubscriberPair(renderer.onEvent, ingestion.onEvent, eventParams, onRendererError, isRendererClosed);
162
+ if (event.type === EventType.RUN_STARTED) {
163
+ await invokeSubscriberPair(renderer.onRunStartedEvent, ingestion.onRunStartedEvent, eventParams, onRendererError, isRendererClosed);
164
+ }
165
+ else if (event.type === EventType.RUN_FINISHED) {
166
+ await invokeSubscriberPair(renderer.onRunFinishedEvent, ingestion.onRunFinishedEvent, {
167
+ ...baseParams,
168
+ event,
169
+ outcome: "success",
170
+ result,
171
+ }, onRendererError, isRendererClosed);
172
+ }
173
+ else if (event.type === EventType.RUN_ERROR) {
174
+ await invokeSubscriberPair(renderer.onRunErrorEvent, ingestion.onRunErrorEvent, eventParams, onRendererError, isRendererClosed);
175
+ }
176
+ }
2
177
  /**
3
178
  * Drive the agent, executing frontend-tool calls and re-invoking until the
4
179
  * agent stops calling them (or we hit the iteration cap). On a captured
@@ -8,55 +183,130 @@ import { parseToolArgs, stringifyHandlerResult } from "./tools.js";
8
183
  */
9
184
  export async function runAgentLoop(args) {
10
185
  const { agent, renderer, tools, toolDescriptors, context, makeToolCtx, handleInterrupt, isAborted, initialResume, } = args;
186
+ let innerRunError;
187
+ let hasDeliveryError = false;
188
+ let deliveryError;
189
+ const deferRendererError = (error) => {
190
+ if (hasDeliveryError)
191
+ return;
192
+ hasDeliveryError = true;
193
+ deliveryError = error;
194
+ };
195
+ const isRendererClosed = () => hasDeliveryError;
196
+ const subscriber = args.subscriber || args.canonicalRun
197
+ ? mergeAgentSubscribers(renderer.subscriber, args.subscriber ?? {}, args.canonicalRun
198
+ ? {
199
+ canonicalRun: args.canonicalRun,
200
+ onInnerRunError: (event) => {
201
+ innerRunError ??= errorFromInnerRun(event);
202
+ },
203
+ onRendererError: deferRendererError,
204
+ isRendererClosed,
205
+ }
206
+ : {})
207
+ : renderer.subscriber;
11
208
  const maxIterations = args.maxIterations ?? 6;
12
209
  const executed = new Set();
13
210
  let resume = initialResume;
14
- for (let i = 0; i < maxIterations; i++) {
15
- if (resume) {
16
- await agent.runAgent({ forwardedProps: { command: resume } }, renderer.subscriber);
17
- resume = undefined;
18
- }
19
- else {
20
- await agent.runAgent({ tools: toolDescriptors, context: context }, renderer.subscriber);
21
- }
22
- if (isAborted?.())
23
- return { iterations: i + 1, interrupted: false };
24
- const pending = renderer.getPendingInterrupt();
25
- if (pending) {
26
- renderer.clearPendingInterrupt();
27
- if (handleInterrupt)
28
- await handleInterrupt(pending);
29
- // ack-first: picker posted; thread.resume re-enters later
30
- return { iterations: i + 1, interrupted: true };
31
- }
32
- const calls = renderer
33
- .getCapturedToolCalls()
34
- .filter((c) => tools.has(c.toolCallName) && !executed.has(c.toolCallId));
35
- if (calls.length === 0)
36
- return { iterations: i + 1, interrupted: false };
37
- ensureAssistantToolCallMessage(agent, calls);
38
- for (const call of calls) {
39
- const tool = tools.get(call.toolCallName);
40
- let result;
41
- const parsed = await parseToolArgs(tool.parameters, call.toolCallArgs);
42
- if (!parsed.ok) {
43
- result = JSON.stringify({
44
- error: `invalid arguments: ${parsed.error}`,
45
- });
211
+ const executeIterations = async () => {
212
+ for (let i = 0; i < maxIterations; i++) {
213
+ if (resume) {
214
+ await agent.runAgent({ forwardedProps: { command: resume } }, subscriber);
215
+ resume = undefined;
46
216
  }
47
217
  else {
48
- try {
49
- result = stringifyHandlerResult(await tool.handler(parsed.value, makeToolCtx(call)));
218
+ await agent.runAgent({ tools: toolDescriptors, context: context }, subscriber);
219
+ }
220
+ if (innerRunError)
221
+ throw innerRunError;
222
+ if (isAborted?.())
223
+ return { iterations: i + 1, interrupted: false };
224
+ const pending = renderer.getPendingInterrupt();
225
+ if (pending) {
226
+ renderer.clearPendingInterrupt();
227
+ if (handleInterrupt)
228
+ await handleInterrupt(pending);
229
+ // ack-first: picker posted; thread.resume re-enters later
230
+ return { iterations: i + 1, interrupted: true };
231
+ }
232
+ const calls = renderer
233
+ .getCapturedToolCalls()
234
+ .filter((c) => tools.has(c.toolCallName) && !executed.has(c.toolCallId));
235
+ if (calls.length === 0)
236
+ return { iterations: i + 1, interrupted: false };
237
+ ensureAssistantToolCallMessage(agent, calls);
238
+ for (const call of calls) {
239
+ const tool = tools.get(call.toolCallName);
240
+ let result;
241
+ const parsed = await parseToolArgs(tool.parameters, call.toolCallArgs);
242
+ if (!parsed.ok) {
243
+ result = JSON.stringify({
244
+ error: `invalid arguments: ${parsed.error}`,
245
+ });
50
246
  }
51
- catch (err) {
52
- result = JSON.stringify({ error: err.message });
247
+ else {
248
+ await args.canonicalRun?.beforeToolCall?.();
249
+ try {
250
+ result = stringifyHandlerResult(await tool.handler(parsed.value, makeToolCtx(call)));
251
+ }
252
+ catch (err) {
253
+ if (isChannelDeliveryTerminatedError(err)) {
254
+ // The provider already terminalled this delivery. Freeze renderer
255
+ // fanout before the canonical RUN_ERROR is ingested, then stop the
256
+ // loop instead of inviting the model to emit through a closed path.
257
+ deferRendererError(err);
258
+ throw err;
259
+ }
260
+ result = JSON.stringify({ error: err.message });
261
+ }
53
262
  }
263
+ pushToolResult(agent, call.toolCallId, result);
264
+ executed.add(call.toolCallId);
54
265
  }
55
- pushToolResult(agent, call.toolCallId, result);
56
- executed.add(call.toolCallId);
57
266
  }
267
+ return { iterations: maxIterations, interrupted: false };
268
+ };
269
+ if (!args.canonicalRun) {
270
+ return executeIterations();
271
+ }
272
+ const startEvent = {
273
+ type: EventType.RUN_STARTED,
274
+ threadId: args.canonicalRun.threadId,
275
+ runId: args.canonicalRun.runId,
276
+ };
277
+ try {
278
+ await emitCanonicalLifecycleEvent(startEvent, undefined, args, deferRendererError, isRendererClosed);
279
+ const result = await executeIterations();
280
+ const finishedEvent = {
281
+ type: EventType.RUN_FINISHED,
282
+ threadId: args.canonicalRun.threadId,
283
+ runId: args.canonicalRun.runId,
284
+ };
285
+ await emitCanonicalLifecycleEvent(finishedEvent, result, args, deferRendererError, isRendererClosed);
286
+ return hasDeliveryError ? { ...result, deliveryError } : result;
287
+ }
288
+ catch (error) {
289
+ const runError = {
290
+ type: EventType.RUN_ERROR,
291
+ threadId: args.canonicalRun.threadId,
292
+ runId: args.canonicalRun.runId,
293
+ message: error instanceof Error ? error.message : String(error),
294
+ ...(typeof error === "object" &&
295
+ error !== null &&
296
+ "code" in error &&
297
+ typeof error.code === "string"
298
+ ? { code: error.code }
299
+ : {}),
300
+ };
301
+ try {
302
+ await emitCanonicalLifecycleEvent(runError, undefined, args, deferRendererError, isRendererClosed);
303
+ }
304
+ catch {
305
+ // Preserve the original run failure after canonical ingestion got its
306
+ // chance to record the terminal error.
307
+ }
308
+ throw error;
58
309
  }
59
- return { iterations: maxIterations, interrupted: false };
60
310
  }
61
311
  /**
62
312
  * If the agent's latest message isn't already the assistant message that
@@ -48,6 +48,43 @@ describe("runAgentLoop", () => {
48
48
  expect(toolResult).toBeDefined();
49
49
  expect(toolResult.toolCallId).toBe("t1");
50
50
  });
51
+ it("keeps ordinary tool failures recoverable by the agent", async () => {
52
+ const renderer = makeFakeRunRenderer();
53
+ const tool = {
54
+ name: "recoverable",
55
+ description: "Fail without closing the delivery.",
56
+ parameters: z.object({}),
57
+ handler: () => {
58
+ throw new Error("try something else");
59
+ },
60
+ };
61
+ const agent = new FakeAgent([
62
+ (sub) => {
63
+ sub.onToolCallEndEvent?.({
64
+ event: { toolCallId: "t1" },
65
+ toolCallName: "recoverable",
66
+ toolCallArgs: {},
67
+ });
68
+ sub.onRunFinishedEvent?.({ event: {} });
69
+ },
70
+ (sub) => {
71
+ sub.onRunFinishedEvent?.({ event: {} });
72
+ },
73
+ ]);
74
+ await runAgentLoop({
75
+ agent,
76
+ renderer,
77
+ tools: new Map([["recoverable", tool]]),
78
+ toolDescriptors,
79
+ context,
80
+ makeToolCtx: () => ({ thread: {}, platform: "fake" }),
81
+ });
82
+ expect(agent.runAgentCalls).toBe(2);
83
+ expect(agent.messages.find(({ role }) => role === "tool")).toMatchObject({
84
+ toolCallId: "t1",
85
+ content: JSON.stringify({ error: "try something else" }),
86
+ });
87
+ });
51
88
  it("posts the picker via handleInterrupt and returns without running tools", async () => {
52
89
  const renderer = makeFakeRunRenderer();
53
90
  const tools = new Map();
@@ -0,0 +1,24 @@
1
+ import type { AbstractAgent } from "@ag-ui/client";
2
+ /**
3
+ * Make an agent tolerate the AG-UI event streams real agents emit.
4
+ *
5
+ * `@ag-ui/langgraph` emits a `TOOL_CALL_START` whose `parentMessageId` is
6
+ * `null` — notably the tool call that triggers an interrupt. The AG-UI schema
7
+ * declares that field optional but never nullable, so `HttpAgent`'s transform
8
+ * re-validates the streamed event, Zod rejects it ("Expected string, received
9
+ * null"), and a single rejected event aborts the entire run. That breaks
10
+ * LangGraph interrupts / human-in-the-loop on every Channel platform.
11
+ *
12
+ * The fix is applied at the transport rather than by replacing the event
13
+ * transform, which keeps everything else the stock path does — protobuf
14
+ * content-type negotiation, `AbortError` → `RUN_ERROR`, and strict validation
15
+ * of every field except the coerced one. Agents that do not stream over HTTP
16
+ * are returned untouched: nothing re-validates their events, so there is
17
+ * nothing to coerce.
18
+ *
19
+ * Channels apply this by default; opt out with
20
+ * `createChannel({ sanitizeAgentEvents: false })`. Remove it once
21
+ * `@ag-ui/langgraph` stops emitting the null (CopilotKit OSS-691).
22
+ */
23
+ export declare function sanitizeAgentEventStream<T extends AbstractAgent>(agent: T): T;
24
+ //# sourceMappingURL=sanitize-agent-events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize-agent-events.d.ts","sourceRoot":"","sources":["../src/sanitize-agent-events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AA8DnD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,SAAS,aAAa,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CA0B7E"}
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Coerce `"<field>": null` to `""` on the wire. Targeted on purpose: we only
3
+ * touch fields where a `null` is known to come through from `@ag-ui/langgraph`
4
+ * and would otherwise trip strict client-side validation.
5
+ *
6
+ * Matching the quoted key means an escaped occurrence inside a JSON string
7
+ * (`\"parentMessageId\":null`, e.g. nested in a `rawEvent` payload) is left
8
+ * alone — the backslash breaks the match.
9
+ */
10
+ const NULLABLE_ID_FIELDS = /("(?:parentMessageId)"\s*:\s*)null/g;
11
+ /** Marks a transport we have already wrapped, so re-entry is a no-op. */
12
+ const SANITIZED = Symbol.for("copilotkit.channels.sanitizedTransport");
13
+ /**
14
+ * Rewrite SSE frames as they stream, coercing the known nullable-string fields.
15
+ *
16
+ * Buffering on the `\n\n` frame delimiter is what makes this safe: a `null`
17
+ * split across two transport chunks is still matched, and every other byte —
18
+ * comments, keep-alives, unrelated fields — is re-emitted verbatim.
19
+ */
20
+ function coerceNullableIds() {
21
+ const decoder = new TextDecoder("utf-8", { fatal: false });
22
+ const encoder = new TextEncoder();
23
+ let buffered = "";
24
+ const coerce = (frame) => encoder.encode(frame.replace(NULLABLE_ID_FIELDS, '$1""'));
25
+ return new TransformStream({
26
+ transform(chunk, controller) {
27
+ buffered += decoder.decode(chunk, { stream: true });
28
+ const frames = buffered.split("\n\n");
29
+ buffered = frames.pop() ?? "";
30
+ for (const frame of frames)
31
+ controller.enqueue(coerce(`${frame}\n\n`));
32
+ },
33
+ flush(controller) {
34
+ buffered += decoder.decode();
35
+ if (buffered)
36
+ controller.enqueue(coerce(buffered));
37
+ },
38
+ });
39
+ }
40
+ /** Does this agent stream its events over HTTP, and therefore re-validate them? */
41
+ function hasHttpTransport(agent) {
42
+ const candidate = agent;
43
+ return (typeof candidate.fetch === "function" && typeof candidate.url === "string");
44
+ }
45
+ /**
46
+ * Make an agent tolerate the AG-UI event streams real agents emit.
47
+ *
48
+ * `@ag-ui/langgraph` emits a `TOOL_CALL_START` whose `parentMessageId` is
49
+ * `null` — notably the tool call that triggers an interrupt. The AG-UI schema
50
+ * declares that field optional but never nullable, so `HttpAgent`'s transform
51
+ * re-validates the streamed event, Zod rejects it ("Expected string, received
52
+ * null"), and a single rejected event aborts the entire run. That breaks
53
+ * LangGraph interrupts / human-in-the-loop on every Channel platform.
54
+ *
55
+ * The fix is applied at the transport rather than by replacing the event
56
+ * transform, which keeps everything else the stock path does — protobuf
57
+ * content-type negotiation, `AbortError` → `RUN_ERROR`, and strict validation
58
+ * of every field except the coerced one. Agents that do not stream over HTTP
59
+ * are returned untouched: nothing re-validates their events, so there is
60
+ * nothing to coerce.
61
+ *
62
+ * Channels apply this by default; opt out with
63
+ * `createChannel({ sanitizeAgentEvents: false })`. Remove it once
64
+ * `@ag-ui/langgraph` stops emitting the null (CopilotKit OSS-691).
65
+ */
66
+ export function sanitizeAgentEventStream(agent) {
67
+ if (!hasHttpTransport(agent))
68
+ return agent;
69
+ const transport = agent.fetch;
70
+ if (transport[SANITIZED])
71
+ return agent;
72
+ const sanitized = async (input, init) => {
73
+ const response = await transport(input, init);
74
+ const contentType = response.headers.get("content-type") ?? "";
75
+ if (!response.ok ||
76
+ !response.body ||
77
+ !contentType.includes("text/event-stream"))
78
+ return response;
79
+ return new Response(response.body.pipeThrough(coerceNullableIds()), {
80
+ status: response.status,
81
+ statusText: response.statusText,
82
+ headers: response.headers,
83
+ });
84
+ };
85
+ sanitized[SANITIZED] = true;
86
+ agent.fetch = sanitized;
87
+ return agent;
88
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=sanitize-agent-events.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize-agent-events.test.d.ts","sourceRoot":"","sources":["../src/sanitize-agent-events.test.ts"],"names":[],"mappings":""}