@copilotkit/runtime 1.56.1 → 1.56.2-canary.pin-to-send

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 (133) hide show
  1. package/dist/graphql/resolvers/copilot.resolver.cjs +2 -1
  2. package/dist/graphql/resolvers/copilot.resolver.cjs.map +1 -1
  3. package/dist/graphql/resolvers/copilot.resolver.mjs +2 -1
  4. package/dist/graphql/resolvers/copilot.resolver.mjs.map +1 -1
  5. package/dist/graphql/resolvers/resolve-message-id.cjs +19 -0
  6. package/dist/graphql/resolvers/resolve-message-id.cjs.map +1 -0
  7. package/dist/graphql/resolvers/resolve-message-id.mjs +18 -0
  8. package/dist/graphql/resolvers/resolve-message-id.mjs.map +1 -0
  9. package/dist/lib/runtime/copilot-runtime.cjs +4 -2
  10. package/dist/lib/runtime/copilot-runtime.cjs.map +1 -1
  11. package/dist/lib/runtime/copilot-runtime.d.cts.map +1 -1
  12. package/dist/lib/runtime/copilot-runtime.d.mts.map +1 -1
  13. package/dist/lib/runtime/copilot-runtime.mjs +4 -2
  14. package/dist/lib/runtime/copilot-runtime.mjs.map +1 -1
  15. package/dist/package.cjs +2 -2
  16. package/dist/package.mjs +2 -2
  17. package/dist/v2/runtime/core/debug-event-bus.cjs +36 -0
  18. package/dist/v2/runtime/core/debug-event-bus.cjs.map +1 -0
  19. package/dist/v2/runtime/core/debug-event-bus.d.cts +19 -0
  20. package/dist/v2/runtime/core/debug-event-bus.d.cts.map +1 -0
  21. package/dist/v2/runtime/core/debug-event-bus.d.mts +19 -0
  22. package/dist/v2/runtime/core/debug-event-bus.d.mts.map +1 -0
  23. package/dist/v2/runtime/core/debug-event-bus.mjs +35 -0
  24. package/dist/v2/runtime/core/debug-event-bus.mjs.map +1 -0
  25. package/dist/v2/runtime/core/fetch-handler.cjs +6 -0
  26. package/dist/v2/runtime/core/fetch-handler.cjs.map +1 -1
  27. package/dist/v2/runtime/core/fetch-handler.d.cts.map +1 -1
  28. package/dist/v2/runtime/core/fetch-handler.d.mts.map +1 -1
  29. package/dist/v2/runtime/core/fetch-handler.mjs +6 -0
  30. package/dist/v2/runtime/core/fetch-handler.mjs.map +1 -1
  31. package/dist/v2/runtime/core/fetch-router.cjs +1 -0
  32. package/dist/v2/runtime/core/fetch-router.cjs.map +1 -1
  33. package/dist/v2/runtime/core/fetch-router.mjs +1 -0
  34. package/dist/v2/runtime/core/fetch-router.mjs.map +1 -1
  35. package/dist/v2/runtime/core/hooks.cjs.map +1 -1
  36. package/dist/v2/runtime/core/hooks.d.cts +2 -0
  37. package/dist/v2/runtime/core/hooks.d.cts.map +1 -1
  38. package/dist/v2/runtime/core/hooks.d.mts +2 -0
  39. package/dist/v2/runtime/core/hooks.d.mts.map +1 -1
  40. package/dist/v2/runtime/core/hooks.mjs.map +1 -1
  41. package/dist/v2/runtime/core/runtime.cjs +5 -0
  42. package/dist/v2/runtime/core/runtime.cjs.map +1 -1
  43. package/dist/v2/runtime/core/runtime.d.cts +5 -0
  44. package/dist/v2/runtime/core/runtime.d.cts.map +1 -1
  45. package/dist/v2/runtime/core/runtime.d.mts +5 -0
  46. package/dist/v2/runtime/core/runtime.d.mts.map +1 -1
  47. package/dist/v2/runtime/core/runtime.mjs +5 -0
  48. package/dist/v2/runtime/core/runtime.mjs.map +1 -1
  49. package/dist/v2/runtime/handlers/handle-connect.cjs +2 -0
  50. package/dist/v2/runtime/handlers/handle-connect.cjs.map +1 -1
  51. package/dist/v2/runtime/handlers/handle-connect.mjs +2 -0
  52. package/dist/v2/runtime/handlers/handle-connect.mjs.map +1 -1
  53. package/dist/v2/runtime/handlers/handle-debug-events.cjs +33 -0
  54. package/dist/v2/runtime/handlers/handle-debug-events.cjs.map +1 -0
  55. package/dist/v2/runtime/handlers/handle-debug-events.mjs +32 -0
  56. package/dist/v2/runtime/handlers/handle-debug-events.mjs.map +1 -0
  57. package/dist/v2/runtime/handlers/handle-run.cjs +1 -0
  58. package/dist/v2/runtime/handlers/handle-run.cjs.map +1 -1
  59. package/dist/v2/runtime/handlers/handle-run.mjs +1 -0
  60. package/dist/v2/runtime/handlers/handle-run.mjs.map +1 -1
  61. package/dist/v2/runtime/handlers/intelligence/connect.cjs +32 -2
  62. package/dist/v2/runtime/handlers/intelligence/connect.cjs.map +1 -1
  63. package/dist/v2/runtime/handlers/intelligence/connect.mjs +31 -2
  64. package/dist/v2/runtime/handlers/intelligence/connect.mjs.map +1 -1
  65. package/dist/v2/runtime/handlers/shared/resolve-intelligence-user.cjs +5 -1
  66. package/dist/v2/runtime/handlers/shared/resolve-intelligence-user.cjs.map +1 -1
  67. package/dist/v2/runtime/handlers/shared/resolve-intelligence-user.mjs +5 -1
  68. package/dist/v2/runtime/handlers/shared/resolve-intelligence-user.mjs.map +1 -1
  69. package/dist/v2/runtime/handlers/shared/sse-response.cjs +21 -1
  70. package/dist/v2/runtime/handlers/shared/sse-response.cjs.map +1 -1
  71. package/dist/v2/runtime/handlers/shared/sse-response.mjs +21 -1
  72. package/dist/v2/runtime/handlers/shared/sse-response.mjs.map +1 -1
  73. package/dist/v2/runtime/handlers/sse/connect.cjs +3 -1
  74. package/dist/v2/runtime/handlers/sse/connect.cjs.map +1 -1
  75. package/dist/v2/runtime/handlers/sse/connect.mjs +3 -1
  76. package/dist/v2/runtime/handlers/sse/connect.mjs.map +1 -1
  77. package/dist/v2/runtime/handlers/sse/run.cjs +3 -1
  78. package/dist/v2/runtime/handlers/sse/run.cjs.map +1 -1
  79. package/dist/v2/runtime/handlers/sse/run.mjs +3 -1
  80. package/dist/v2/runtime/handlers/sse/run.mjs.map +1 -1
  81. package/dist/v2/runtime/intelligence-platform/client.cjs +2 -7
  82. package/dist/v2/runtime/intelligence-platform/client.cjs.map +1 -1
  83. package/dist/v2/runtime/intelligence-platform/client.d.cts +1 -4
  84. package/dist/v2/runtime/intelligence-platform/client.d.cts.map +1 -1
  85. package/dist/v2/runtime/intelligence-platform/client.d.mts +1 -4
  86. package/dist/v2/runtime/intelligence-platform/client.d.mts.map +1 -1
  87. package/dist/v2/runtime/intelligence-platform/client.mjs +2 -7
  88. package/dist/v2/runtime/intelligence-platform/client.mjs.map +1 -1
  89. package/dist/v2/runtime/runner/intelligence.cjs +17 -5
  90. package/dist/v2/runtime/runner/intelligence.cjs.map +1 -1
  91. package/dist/v2/runtime/runner/intelligence.d.cts +1 -0
  92. package/dist/v2/runtime/runner/intelligence.d.cts.map +1 -1
  93. package/dist/v2/runtime/runner/intelligence.d.mts +1 -0
  94. package/dist/v2/runtime/runner/intelligence.d.mts.map +1 -1
  95. package/dist/v2/runtime/runner/intelligence.mjs +17 -5
  96. package/dist/v2/runtime/runner/intelligence.mjs.map +1 -1
  97. package/package.json +3 -3
  98. package/src/agents/langgraph/__tests__/event-source.test.ts +256 -0
  99. package/src/graphql/resolvers/__tests__/resolve-message-id.test.ts +25 -0
  100. package/src/graphql/resolvers/copilot.resolver.ts +2 -1
  101. package/src/graphql/resolvers/resolve-message-id.ts +14 -0
  102. package/src/lib/runtime/__tests__/handle-service-adapter.test.ts +108 -0
  103. package/src/lib/runtime/__tests__/retry-utils.test.ts +137 -0
  104. package/src/lib/runtime/agent-integrations/langgraph/__tests__/dispatch-event-filtering.test.ts +190 -0
  105. package/src/lib/runtime/copilot-runtime.ts +20 -4
  106. package/src/lib/runtime/retry-utils.ts +41 -1
  107. package/src/v2/runtime/__tests__/fetch-router.test.ts +22 -0
  108. package/src/v2/runtime/__tests__/handle-connect.test.ts +58 -5
  109. package/src/v2/runtime/__tests__/handle-run.test.ts +31 -4
  110. package/src/v2/runtime/__tests__/handle-threads.test.ts +66 -4
  111. package/src/v2/runtime/__tests__/integration/node-servers.integration.test.ts +19 -0
  112. package/src/v2/runtime/__tests__/integration/suites/debug-events.suite.ts +253 -0
  113. package/src/v2/runtime/__tests__/runtime.test.ts +3 -1
  114. package/src/v2/runtime/core/__tests__/debug-event-bus.test.ts +156 -0
  115. package/src/v2/runtime/core/debug-event-bus.ts +45 -0
  116. package/src/v2/runtime/core/fetch-handler.ts +4 -0
  117. package/src/v2/runtime/core/fetch-router.ts +11 -0
  118. package/src/v2/runtime/core/hooks.ts +2 -1
  119. package/src/v2/runtime/core/runtime.ts +12 -0
  120. package/src/v2/runtime/handlers/__tests__/handle-debug-events.test.ts +176 -0
  121. package/src/v2/runtime/handlers/handle-connect.ts +2 -0
  122. package/src/v2/runtime/handlers/handle-debug-events.ts +52 -0
  123. package/src/v2/runtime/handlers/handle-run.ts +1 -0
  124. package/src/v2/runtime/handlers/intelligence/connect.ts +58 -1
  125. package/src/v2/runtime/handlers/shared/resolve-intelligence-user.ts +4 -1
  126. package/src/v2/runtime/handlers/shared/sse-response.ts +46 -0
  127. package/src/v2/runtime/handlers/sse/__tests__/sse-connect-agent-id.test.ts +71 -0
  128. package/src/v2/runtime/handlers/sse/connect.ts +6 -0
  129. package/src/v2/runtime/handlers/sse/run.ts +4 -0
  130. package/src/v2/runtime/intelligence-platform/__tests__/client.test.ts +13 -11
  131. package/src/v2/runtime/intelligence-platform/client.ts +3 -11
  132. package/src/v2/runtime/runner/__tests__/intelligence-runner.test.ts +51 -1
  133. package/src/v2/runtime/runner/intelligence.ts +27 -9
@@ -0,0 +1,253 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
2
+ import type { ServerHandle } from "../servers/types";
3
+ import { readSSEStream } from "../helpers/sse-reader";
4
+
5
+ /**
6
+ * Envelope shape returned in the debug SSE stream.
7
+ */
8
+ interface DebugEnvelope {
9
+ timestamp: number;
10
+ agentId: string;
11
+ threadId: string;
12
+ runId: string;
13
+ event: { type: string; [key: string]: unknown };
14
+ }
15
+
16
+ /**
17
+ * Parse debug envelopes from SSE payload text.
18
+ * Each `data:` line contains a JSON DebugEventEnvelope.
19
+ */
20
+ function parseDebugEnvelopes(ssePayload: string): DebugEnvelope[] {
21
+ const envelopes: DebugEnvelope[] = [];
22
+ for (const line of ssePayload.split("\n")) {
23
+ if (!line.startsWith("data:")) continue;
24
+ const json = line.slice("data:".length).trim();
25
+ if (!json) continue;
26
+ try {
27
+ envelopes.push(JSON.parse(json));
28
+ } catch {
29
+ // skip malformed lines
30
+ }
31
+ }
32
+ return envelopes;
33
+ }
34
+
35
+ /**
36
+ * Read from a long-lived debug SSE stream until we see a RUN_FINISHED envelope
37
+ * or a timeout elapses. Returns the raw text accumulated.
38
+ *
39
+ * IMPORTANT: The debug SSE stream is long-lived and never closes on its own.
40
+ * We MUST use a timeout to stop reading, and cancel the reader afterwards.
41
+ */
42
+ async function readDebugStream(
43
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
+ stream: ReadableStream<any>,
45
+ opts: { waitMs?: number } = {},
46
+ ): Promise<string> {
47
+ const waitMs = opts.waitMs ?? 4_000;
48
+ const reader = stream.getReader();
49
+ const decoder = new TextDecoder();
50
+ let output = "";
51
+ let stopped = false;
52
+
53
+ const timer = setTimeout(() => {
54
+ stopped = true;
55
+ reader.cancel().catch(() => {});
56
+ }, waitMs);
57
+
58
+ try {
59
+ while (!stopped) {
60
+ const result = await reader.read().catch(() => ({
61
+ done: true as const,
62
+ value: undefined,
63
+ }));
64
+ if (result.done) break;
65
+ if (result.value) {
66
+ output +=
67
+ typeof result.value === "string"
68
+ ? result.value
69
+ : decoder.decode(result.value as Uint8Array, { stream: true });
70
+ if (output.includes("RUN_FINISHED")) {
71
+ stopped = true;
72
+ break;
73
+ }
74
+ }
75
+ }
76
+ } finally {
77
+ clearTimeout(timer);
78
+ // Do NOT await reader.cancel() — on tee'd streams (created by response.clone()
79
+ // inside the fetch handler), awaiting cancel hangs indefinitely because the
80
+ // other tee branch is never consumed.
81
+ reader.cancel().catch(() => {});
82
+ output += decoder.decode();
83
+ }
84
+
85
+ return output;
86
+ }
87
+
88
+ /**
89
+ * Shared debug-events integration test suite.
90
+ *
91
+ * @param name Display name, e.g. "Express"
92
+ * @param factory Creates & starts the server; returns a handle
93
+ */
94
+ export function debugEventsSuite(
95
+ name: string,
96
+ factory: (opts?: {
97
+ capturedHeaders?: Record<string, string>[];
98
+ }) => Promise<ServerHandle & { handler?: (r: Request) => Promise<Response> }>,
99
+ ) {
100
+ describe(`[${name}] Debug Events`, () => {
101
+ let handle: ServerHandle & { handler?: (r: Request) => Promise<Response> };
102
+ let doFetch: (
103
+ input: RequestInfo | URL,
104
+ init?: RequestInit,
105
+ ) => Promise<Response>;
106
+
107
+ beforeAll(async () => {
108
+ handle = await factory();
109
+ doFetch = handle.handler
110
+ ? (input, init) =>
111
+ handle.handler!(
112
+ new Request(
113
+ typeof input === "string" || input instanceof URL
114
+ ? input
115
+ : input,
116
+ init,
117
+ ),
118
+ )
119
+ : fetch;
120
+ });
121
+
122
+ afterAll(async () => {
123
+ await handle?.close();
124
+ });
125
+
126
+ const url = (path: string) => `${handle.baseUrl}${handle.basePath}${path}`;
127
+
128
+ // ─── SSE Format + Events + Envelope Structure ────────────────────
129
+ // Combined into a single test to avoid orphaned debug-stream subscribers
130
+ // across tests (the debug SSE endpoint is long-lived and its cleanup
131
+ // depends on the request signal being aborted).
132
+
133
+ it("streams debug event envelopes with correct structure during an agent run", async () => {
134
+ const controller = new AbortController();
135
+
136
+ // Start the debug stream. For real HTTP servers, fetch blocks until
137
+ // the first chunk arrives, so we also start the agent run concurrently.
138
+ const debugFetchPromise = doFetch(url("/cpk-debug-events"), {
139
+ signal: controller.signal,
140
+ });
141
+
142
+ // Give the subscription a tick to register
143
+ await new Promise((r) => setTimeout(r, 50));
144
+
145
+ // Trigger an agent run. We start it AND begin consuming its stream
146
+ // concurrently with reading the debug stream.
147
+ const runRes = await doFetch(url("/agent/default/run"), {
148
+ method: "POST",
149
+ headers: { "Content-Type": "application/json" },
150
+ body: JSON.stringify({
151
+ threadId: "t-debug-1",
152
+ runId: "r-debug-1",
153
+ messages: [],
154
+ state: {},
155
+ tools: [],
156
+ context: [],
157
+ forwardedProps: {},
158
+ }),
159
+ });
160
+
161
+ // Consume the run stream and the debug stream concurrently.
162
+ // Both are needed: the debug stream blocks until events arrive,
163
+ // and the run stream must be consumed to avoid backpressure.
164
+ const [debugRes, runPayload] = await Promise.all([
165
+ debugFetchPromise,
166
+ readSSEStream(runRes.body!),
167
+ ]);
168
+
169
+ // ── SSE response format ──
170
+ expect(debugRes.status).toBe(200);
171
+ expect(debugRes.headers.get("content-type")).toContain(
172
+ "text/event-stream",
173
+ );
174
+
175
+ // Run completed — events should be buffered in the debug stream.
176
+ expect(runPayload).toContain("RUN_FINISHED");
177
+
178
+ // Read the debug stream (events are already in the buffer)
179
+ const debugPayload = await readDebugStream(debugRes.body!, {
180
+ waitMs: 4_000,
181
+ });
182
+
183
+ const envelopes = parseDebugEnvelopes(debugPayload);
184
+
185
+ // ── Events flow through ──
186
+ expect(envelopes.length).toBeGreaterThan(0);
187
+
188
+ const eventTypes = envelopes.map((e) => e.event.type);
189
+ expect(eventTypes).toContain("RUN_STARTED");
190
+ expect(eventTypes).toContain("RUN_FINISHED");
191
+
192
+ // ── Envelope structure ──
193
+ for (const envelope of envelopes) {
194
+ expect(typeof envelope.timestamp).toBe("number");
195
+ expect(envelope.timestamp).toBeGreaterThan(0);
196
+ expect(envelope.agentId).toBe("default");
197
+ expect(typeof envelope.threadId).toBe("string");
198
+ expect(typeof envelope.runId).toBe("string");
199
+ expect(envelope.event).toBeDefined();
200
+ expect(typeof envelope.event.type).toBe("string");
201
+ }
202
+
203
+ // Full event sequence
204
+ expect(eventTypes).toContain("TEXT_MESSAGE_START");
205
+ expect(eventTypes).toContain("TEXT_MESSAGE_CONTENT");
206
+ expect(eventTypes).toContain("TEXT_MESSAGE_END");
207
+
208
+ // Clean up: abort the request so the debug subscriber is removed
209
+ controller.abort();
210
+ }, 15_000);
211
+
212
+ // Regression guard for agentId forwarding on /connect lives in a
213
+ // dedicated unit test (sse-connect-agent-id.test.ts) — driving /connect
214
+ // through the integration runtime doesn't emit events in a
215
+ // test-friendly way, so the unit test feeds a synthetic observable
216
+ // into handleSseConnect and asserts the envelope carries the route
217
+ // agentId rather than the literal string "connect".
218
+
219
+ // ─── HTTP Method Validation ──────────────────────────────────────
220
+
221
+ it("POST /cpk-debug-events returns 405", async () => {
222
+ const res = await doFetch(url("/cpk-debug-events"), { method: "POST" });
223
+ expect(res.status).toBe(405);
224
+ });
225
+ });
226
+ }
227
+
228
+ /**
229
+ * Production guard test -- only needs the fetch-direct handler since
230
+ * it doesn't require a real server. Tests that NODE_ENV=production
231
+ * returns 404 for the debug-events endpoint.
232
+ */
233
+ export function debugEventsProductionGuardSuite(
234
+ createHandler: () => { handler: (r: Request) => Promise<Response> },
235
+ baseUrl: string,
236
+ basePath: string,
237
+ ) {
238
+ describe("[Fetch] Debug Events – production guard", () => {
239
+ it("returns 404 when NODE_ENV=production", async () => {
240
+ const originalEnv = process.env.NODE_ENV;
241
+ try {
242
+ process.env.NODE_ENV = "production";
243
+ const { handler } = createHandler();
244
+ const res = await handler(
245
+ new Request(`${baseUrl}${basePath}/cpk-debug-events`),
246
+ );
247
+ expect(res.status).toBe(404);
248
+ } finally {
249
+ process.env.NODE_ENV = originalEnv;
250
+ }
251
+ });
252
+ });
253
+ }
@@ -11,7 +11,9 @@ import { IntelligenceAgentRunner } from "../runner/intelligence";
11
11
 
12
12
  describe("runtime construction", () => {
13
13
  const agents = {};
14
- const identifyUser = vi.fn().mockResolvedValue({ id: "user-1" });
14
+ const identifyUser = vi
15
+ .fn()
16
+ .mockResolvedValue({ id: "user-1", name: "User One" });
15
17
  const createMockIntelligence = (): CopilotKitIntelligence =>
16
18
  ({
17
19
  ɵgetRunnerWsUrl: vi.fn().mockReturnValue("ws://runner.example"),
@@ -0,0 +1,156 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { EventType } from "@ag-ui/client";
3
+ import type { BaseEvent } from "@ag-ui/client";
4
+ import type { DebugEventEnvelope } from "@copilotkit/shared";
5
+ import { DebugEventBus } from "../debug-event-bus";
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Helpers
9
+ // ---------------------------------------------------------------------------
10
+
11
+ function createBaseEvent(
12
+ overrides: Partial<BaseEvent> & { type: EventType } = {
13
+ type: EventType.RUN_STARTED,
14
+ },
15
+ ): BaseEvent {
16
+ return { type: overrides.type, ...overrides };
17
+ }
18
+
19
+ const defaultMetadata = {
20
+ agentId: "test-agent",
21
+ threadId: "thread-1",
22
+ runId: "run-1",
23
+ };
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Tests
27
+ // ---------------------------------------------------------------------------
28
+
29
+ describe("DebugEventBus", () => {
30
+ it("subscribe adds a listener and broadcast calls it with the correct envelope shape", () => {
31
+ const bus = new DebugEventBus();
32
+ const listener = vi.fn<[DebugEventEnvelope], void>();
33
+ const event = createBaseEvent({ type: EventType.RUN_STARTED });
34
+
35
+ bus.subscribe(listener);
36
+ bus.broadcast(event, defaultMetadata);
37
+
38
+ expect(listener).toHaveBeenCalledOnce();
39
+ const envelope = listener.mock.calls[0][0];
40
+ expect(envelope).toEqual(
41
+ expect.objectContaining({
42
+ agentId: "test-agent",
43
+ threadId: "thread-1",
44
+ runId: "run-1",
45
+ event,
46
+ }),
47
+ );
48
+ expect(typeof envelope.timestamp).toBe("number");
49
+ });
50
+
51
+ it("unsubscribe removes the listener so subsequent broadcasts don't reach it", () => {
52
+ const bus = new DebugEventBus();
53
+ const listener = vi.fn<[DebugEventEnvelope], void>();
54
+ const event = createBaseEvent();
55
+
56
+ const unsub = bus.subscribe(listener);
57
+ unsub();
58
+ bus.broadcast(event, defaultMetadata);
59
+
60
+ expect(listener).not.toHaveBeenCalled();
61
+ });
62
+
63
+ it("multiple listeners all receive the same broadcast", () => {
64
+ const bus = new DebugEventBus();
65
+ const listenerA = vi.fn<[DebugEventEnvelope], void>();
66
+ const listenerB = vi.fn<[DebugEventEnvelope], void>();
67
+ const event = createBaseEvent();
68
+
69
+ bus.subscribe(listenerA);
70
+ bus.subscribe(listenerB);
71
+ bus.broadcast(event, defaultMetadata);
72
+
73
+ expect(listenerA).toHaveBeenCalledOnce();
74
+ expect(listenerB).toHaveBeenCalledOnce();
75
+ // Both receive the same envelope object
76
+ expect(listenerA.mock.calls[0][0]).toBe(listenerB.mock.calls[0][0]);
77
+ });
78
+
79
+ it("listener errors are swallowed and other listeners still receive the event", () => {
80
+ const bus = new DebugEventBus();
81
+ const failingListener = vi
82
+ .fn<[DebugEventEnvelope], void>()
83
+ .mockImplementation(() => {
84
+ throw new Error("boom");
85
+ });
86
+ const healthyListener = vi.fn<[DebugEventEnvelope], void>();
87
+ const event = createBaseEvent();
88
+
89
+ bus.subscribe(failingListener);
90
+ bus.subscribe(healthyListener);
91
+
92
+ // Should not throw
93
+ expect(() => bus.broadcast(event, defaultMetadata)).not.toThrow();
94
+ expect(failingListener).toHaveBeenCalledOnce();
95
+ expect(healthyListener).toHaveBeenCalledOnce();
96
+ });
97
+
98
+ it("broadcasting with no listeners does not throw", () => {
99
+ const bus = new DebugEventBus();
100
+ const event = createBaseEvent();
101
+
102
+ expect(() => bus.broadcast(event, defaultMetadata)).not.toThrow();
103
+ });
104
+
105
+ it("listenerCount reflects current count after subscribe and unsubscribe", () => {
106
+ const bus = new DebugEventBus();
107
+
108
+ expect(bus.listenerCount).toBe(0);
109
+
110
+ const unsub1 = bus.subscribe(vi.fn());
111
+ expect(bus.listenerCount).toBe(1);
112
+
113
+ const unsub2 = bus.subscribe(vi.fn());
114
+ expect(bus.listenerCount).toBe(2);
115
+
116
+ unsub1();
117
+ expect(bus.listenerCount).toBe(1);
118
+
119
+ unsub2();
120
+ expect(bus.listenerCount).toBe(0);
121
+ });
122
+
123
+ it("envelope has correct timestamp, agentId, threadId, runId, and the original event", () => {
124
+ const bus = new DebugEventBus();
125
+ const listener = vi.fn<[DebugEventEnvelope], void>();
126
+ const event = createBaseEvent({ type: EventType.STEP_STARTED });
127
+ const metadata = { agentId: "agent-x", threadId: "t-42", runId: "r-99" };
128
+
129
+ const before = Date.now();
130
+ bus.subscribe(listener);
131
+ bus.broadcast(event, metadata);
132
+ const after = Date.now();
133
+
134
+ const envelope = listener.mock.calls[0][0];
135
+ expect(envelope.agentId).toBe("agent-x");
136
+ expect(envelope.threadId).toBe("t-42");
137
+ expect(envelope.runId).toBe("r-99");
138
+ expect(envelope.event).toBe(event);
139
+ expect(envelope.timestamp).toBeGreaterThanOrEqual(before);
140
+ expect(envelope.timestamp).toBeLessThanOrEqual(after);
141
+ });
142
+
143
+ it("RUN_STARTED event type passthrough: envelope.event is the original object, not a copy", () => {
144
+ const bus = new DebugEventBus();
145
+ const listener = vi.fn<[DebugEventEnvelope], void>();
146
+ const event = createBaseEvent({ type: EventType.RUN_STARTED });
147
+
148
+ bus.subscribe(listener);
149
+ bus.broadcast(event, defaultMetadata);
150
+
151
+ const envelope = listener.mock.calls[0][0];
152
+ // Strict referential equality — the event is passed through, not cloned
153
+ expect(envelope.event).toBe(event);
154
+ expect(envelope.event.type).toBe(EventType.RUN_STARTED);
155
+ });
156
+ });
@@ -0,0 +1,45 @@
1
+ import { BaseEvent } from "@ag-ui/client";
2
+ import { DebugEventEnvelope } from "@copilotkit/shared";
3
+
4
+ export type DebugEventListener = (envelope: DebugEventEnvelope) => void;
5
+
6
+ export class DebugEventBus {
7
+ private listeners = new Set<DebugEventListener>();
8
+
9
+ subscribe(listener: DebugEventListener): () => void {
10
+ this.listeners.add(listener);
11
+ return () => {
12
+ this.listeners.delete(listener);
13
+ };
14
+ }
15
+
16
+ broadcast(
17
+ event: BaseEvent,
18
+ metadata: { agentId: string; threadId: string; runId: string },
19
+ ): void {
20
+ if (this.listeners.size === 0) return;
21
+
22
+ const envelope: DebugEventEnvelope = {
23
+ timestamp: Date.now(),
24
+ agentId: metadata.agentId,
25
+ threadId: metadata.threadId,
26
+ runId: metadata.runId,
27
+ event,
28
+ };
29
+
30
+ for (const listener of this.listeners) {
31
+ try {
32
+ listener(envelope);
33
+ } catch (err) {
34
+ console.warn(
35
+ "[DebugEventBus] Listener error suppressed:",
36
+ err instanceof Error ? err.message : err,
37
+ );
38
+ }
39
+ }
40
+ }
41
+
42
+ get listenerCount(): number {
43
+ return this.listeners.size;
44
+ }
45
+ }
@@ -46,6 +46,7 @@ import { handleConnectAgent } from "../handlers/handle-connect";
46
46
  import { handleStopAgent } from "../handlers/handle-stop";
47
47
  import { handleGetRuntimeInfo } from "../handlers/get-runtime-info";
48
48
  import { handleTranscribe } from "../handlers/handle-transcribe";
49
+ import { handleDebugEvents } from "../handlers/handle-debug-events";
49
50
  import {
50
51
  handleListThreads,
51
52
  handleSubscribeToThreads,
@@ -339,6 +340,8 @@ function dispatchRoute(
339
340
  request,
340
341
  threadId: route.threadId,
341
342
  });
343
+ case "cpk-debug-events":
344
+ return Promise.resolve(handleDebugEvents({ runtime, request }));
342
345
  }
343
346
  }
344
347
 
@@ -414,6 +417,7 @@ function validateHttpMethod(
414
417
  case "info":
415
418
  case "threads/list":
416
419
  case "threads/messages":
420
+ case "cpk-debug-events":
417
421
  if (method === "GET") return null;
418
422
  return jsonResponse({ error: "Method not allowed" }, 405, {
419
423
  Allow: "GET",
@@ -74,6 +74,17 @@ function matchSegments(path: string): RouteInfo | null {
74
74
  return { method: "transcribe" };
75
75
  }
76
76
 
77
+ // /cpk-debug-events (1 segment)
78
+ // Reserved route name: the `cpk-` prefix makes collision with a
79
+ // user-named agent essentially impossible (the router only treats
80
+ // `agent/:agentId/...` patterns as agent lookups, so a bare
81
+ // `cpk-debug-events` segment would never fall through to one —
82
+ // the prefix is the real guard, not this branch's position).
83
+ // Handler returns 404 in production.
84
+ if (len >= 1 && segments[len - 1] === "cpk-debug-events") {
85
+ return { method: "cpk-debug-events" };
86
+ }
87
+
77
88
  // /agent/:agentId/run (3 segments)
78
89
  if (
79
90
  len >= 3 &&
@@ -43,7 +43,8 @@ export type RouteInfo =
43
43
  | { method: "threads/subscribe" }
44
44
  | { method: "threads/update"; threadId: string }
45
45
  | { method: "threads/archive"; threadId: string }
46
- | { method: "threads/messages"; threadId: string };
46
+ | { method: "threads/messages"; threadId: string }
47
+ | { method: "cpk-debug-events" };
47
48
 
48
49
  /* ------------------------------------------------------------------------------------------------
49
50
  * Hook contexts
@@ -24,6 +24,7 @@ import type {
24
24
  } from "./middleware";
25
25
  import { createLogger, type CopilotRuntimeLogger } from "../../../lib/logger";
26
26
  import { TranscriptionService } from "../transcription-service/transcription-service";
27
+ import { DebugEventBus } from "./debug-event-bus";
27
28
  import { AgentRunner } from "../runner/agent-runner";
28
29
  import { InMemoryAgentRunner } from "../runner/in-memory";
29
30
  import { IntelligenceAgentRunner } from "../runner/intelligence";
@@ -140,6 +141,7 @@ interface BaseCopilotRuntimeOptions extends CopilotRuntimeMiddlewares {
140
141
 
141
142
  export interface CopilotRuntimeUser {
142
143
  id: string;
144
+ name: string;
143
145
  }
144
146
 
145
147
  export type IdentifyUserCallback = (
@@ -189,6 +191,7 @@ export interface CopilotRuntimeLike {
189
191
  identifyUser?: IdentifyUserCallback;
190
192
  mode: RuntimeMode;
191
193
  licenseChecker?: LicenseChecker;
194
+ debugEventBus?: DebugEventBus;
192
195
  debug: ResolvedDebugConfig;
193
196
  debugLogger?: CopilotRuntimeLogger;
194
197
  }
@@ -218,6 +221,7 @@ abstract class BaseCopilotRuntime implements CopilotRuntimeLike {
218
221
  public mcpApps: CopilotRuntimeOptions["mcpApps"];
219
222
  public openGenerativeUI: CopilotRuntimeOptions["openGenerativeUI"];
220
223
  public licenseChecker?: LicenseChecker;
224
+ public readonly debugEventBus?: DebugEventBus;
221
225
  public debug: ResolvedDebugConfig;
222
226
  public debugLogger?: CopilotRuntimeLogger;
223
227
 
@@ -243,6 +247,10 @@ abstract class BaseCopilotRuntime implements CopilotRuntimeLike {
243
247
  this.mcpApps = mcpApps;
244
248
  this.openGenerativeUI = openGenerativeUI;
245
249
  this.runner = runner;
250
+
251
+ if (process.env.NODE_ENV !== "production") {
252
+ this.debugEventBus = new DebugEventBus();
253
+ }
246
254
  this.debug = resolveDebugConfig(options.debug);
247
255
  if (this.debug.enabled) {
248
256
  this.debugLogger = createLogger({
@@ -407,6 +415,10 @@ export class CopilotRuntime implements CopilotRuntimeLike {
407
415
  return this.delegate.licenseChecker;
408
416
  }
409
417
 
418
+ get debugEventBus() {
419
+ return this.delegate.debugEventBus;
420
+ }
421
+
410
422
  get debug(): ResolvedDebugConfig {
411
423
  return this.delegate.debug;
412
424
  }