@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,176 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { handleDebugEvents } from "../handle-debug-events";
3
+ import { DebugEventBus } from "../../core/debug-event-bus";
4
+ import type { CopilotRuntimeLike } from "../../core/runtime";
5
+ import type { BaseEvent } from "@ag-ui/client";
6
+
7
+ /* ------------------------------------------------------------------------------------------------
8
+ * Helpers
9
+ * --------------------------------------------------------------------------------------------- */
10
+
11
+ function createMockRuntime(
12
+ overrides: { debugEventBus?: DebugEventBus } = {},
13
+ ): Pick<CopilotRuntimeLike, "debugEventBus"> {
14
+ return {
15
+ debugEventBus: overrides.debugEventBus ?? new DebugEventBus(),
16
+ };
17
+ }
18
+
19
+ function createMockRequest(options: { signal?: AbortSignal } = {}): Request {
20
+ return new Request("http://localhost/cpk-debug-events", {
21
+ method: "GET",
22
+ signal: options.signal,
23
+ });
24
+ }
25
+
26
+ function createTestEvent(): BaseEvent {
27
+ return { type: "custom" } as BaseEvent;
28
+ }
29
+
30
+ async function readNextSSELine(
31
+ reader: ReadableStreamDefaultReader<Uint8Array>,
32
+ ): Promise<string> {
33
+ const decoder = new TextDecoder();
34
+ let accumulated = "";
35
+ while (true) {
36
+ const { value, done } = await reader.read();
37
+ if (done) break;
38
+ accumulated += decoder.decode(value, { stream: true });
39
+ if (accumulated.includes("\n\n")) return accumulated;
40
+ }
41
+ return accumulated;
42
+ }
43
+
44
+ /* ------------------------------------------------------------------------------------------------
45
+ * Tests
46
+ * --------------------------------------------------------------------------------------------- */
47
+
48
+ describe("handleDebugEvents", () => {
49
+ let savedNodeEnv: string | undefined;
50
+
51
+ beforeEach(() => {
52
+ savedNodeEnv = process.env.NODE_ENV;
53
+ });
54
+
55
+ afterEach(() => {
56
+ if (savedNodeEnv === undefined) {
57
+ delete process.env.NODE_ENV;
58
+ } else {
59
+ process.env.NODE_ENV = savedNodeEnv;
60
+ }
61
+ });
62
+
63
+ it("returns 404 when NODE_ENV is production", async () => {
64
+ process.env.NODE_ENV = "production";
65
+
66
+ const runtime = createMockRuntime();
67
+ const request = createMockRequest();
68
+
69
+ const response = handleDebugEvents({
70
+ runtime: runtime as CopilotRuntimeLike,
71
+ request,
72
+ });
73
+
74
+ expect(response.status).toBe(404);
75
+ expect(await response.text()).toBe("Not Found");
76
+ });
77
+
78
+ it("returns 503 when debugEventBus is undefined", async () => {
79
+ process.env.NODE_ENV = "test";
80
+
81
+ const runtime = createMockRuntime({ debugEventBus: undefined });
82
+ // Remove the property so it's truly undefined
83
+ delete (runtime as Record<string, unknown>).debugEventBus;
84
+
85
+ const request = createMockRequest();
86
+
87
+ const response = handleDebugEvents({
88
+ runtime: runtime as CopilotRuntimeLike,
89
+ request,
90
+ });
91
+
92
+ expect(response.status).toBe(503);
93
+ expect(await response.text()).toBe("Debug event bus not available");
94
+ });
95
+
96
+ it("returns correct SSE response headers", () => {
97
+ process.env.NODE_ENV = "test";
98
+
99
+ const runtime = createMockRuntime();
100
+ const request = createMockRequest();
101
+
102
+ const response = handleDebugEvents({
103
+ runtime: runtime as CopilotRuntimeLike,
104
+ request,
105
+ });
106
+
107
+ expect(response.status).toBe(200);
108
+ expect(response.headers.get("Content-Type")).toBe("text/event-stream");
109
+ expect(response.headers.get("Cache-Control")).toBe("no-cache");
110
+ expect(response.headers.get("Connection")).toBe("keep-alive");
111
+ });
112
+
113
+ it("streams events as SSE data lines when bus broadcasts", async () => {
114
+ process.env.NODE_ENV = "test";
115
+
116
+ const bus = new DebugEventBus();
117
+ const runtime = createMockRuntime({ debugEventBus: bus });
118
+ const request = createMockRequest();
119
+
120
+ const response = handleDebugEvents({
121
+ runtime: runtime as CopilotRuntimeLike,
122
+ request,
123
+ });
124
+
125
+ const reader = response.body!.getReader();
126
+
127
+ // Read and discard the initial ": connected" SSE comment
128
+ const comment = await readNextSSELine(reader);
129
+ expect(comment).toMatch(/^: connected/);
130
+
131
+ // Broadcast an event through the bus
132
+ bus.broadcast(createTestEvent(), {
133
+ agentId: "agent-1",
134
+ threadId: "thread-1",
135
+ runId: "run-1",
136
+ });
137
+
138
+ const line = await readNextSSELine(reader);
139
+
140
+ // The line should be "data: {json}\n\n"
141
+ expect(line).toMatch(/^data: \{.*\}\n\n$/);
142
+
143
+ const parsed = JSON.parse(line.replace("data: ", "").trim());
144
+ expect(parsed.agentId).toBe("agent-1");
145
+ expect(parsed.threadId).toBe("thread-1");
146
+ expect(parsed.runId).toBe("run-1");
147
+ expect(parsed.event).toEqual(createTestEvent());
148
+ expect(typeof parsed.timestamp).toBe("number");
149
+
150
+ reader.releaseLock();
151
+ });
152
+
153
+ it("unsubscribes from bus when request is aborted", async () => {
154
+ process.env.NODE_ENV = "test";
155
+
156
+ const bus = new DebugEventBus();
157
+ const runtime = createMockRuntime({ debugEventBus: bus });
158
+
159
+ const abortController = new AbortController();
160
+ const request = createMockRequest({ signal: abortController.signal });
161
+
162
+ handleDebugEvents({
163
+ runtime: runtime as CopilotRuntimeLike,
164
+ request,
165
+ });
166
+
167
+ // Before abort, the bus should have one listener
168
+ expect(bus.listenerCount).toBe(1);
169
+
170
+ // Abort the request
171
+ abortController.abort();
172
+
173
+ // After abort, the listener should be cleaned up
174
+ expect(bus.listenerCount).toBe(0);
175
+ });
176
+ });
@@ -44,6 +44,7 @@ export async function handleConnectAgent({
44
44
  runtime,
45
45
  request,
46
46
  threadId: connectRequest.input.threadId,
47
+ runId: connectRequest.input.runId,
47
48
  lastSeenEventId: connectRequest.lastSeenEventId,
48
49
  });
49
50
  }
@@ -51,6 +52,7 @@ export async function handleConnectAgent({
51
52
  return handleSseConnect({
52
53
  runtime,
53
54
  request,
55
+ agentId,
54
56
  threadId: connectRequest.input.threadId,
55
57
  });
56
58
  } catch (error) {
@@ -0,0 +1,52 @@
1
+ import { CopilotRuntimeLike } from "../core/runtime";
2
+ import { DebugEventEnvelope } from "@copilotkit/shared";
3
+
4
+ interface HandleDebugEventsParams {
5
+ runtime: CopilotRuntimeLike;
6
+ request: Request;
7
+ }
8
+
9
+ export function handleDebugEvents({
10
+ runtime,
11
+ request,
12
+ }: HandleDebugEventsParams): Response {
13
+ if (process.env.NODE_ENV === "production") {
14
+ return new Response("Not Found", { status: 404 });
15
+ }
16
+
17
+ if (!runtime.debugEventBus) {
18
+ return new Response("Debug event bus not available", { status: 503 });
19
+ }
20
+
21
+ const bus = runtime.debugEventBus;
22
+ const encoder = new TextEncoder();
23
+ const stream = new TransformStream();
24
+ const writer = stream.writable.getWriter();
25
+
26
+ // Send an SSE comment immediately to flush response headers to the client.
27
+ // Without this, some frameworks buffer the response until actual data is written,
28
+ // leaving the client stuck in "connecting" state.
29
+ writer.write(encoder.encode(": connected\n\n")).catch(() => {});
30
+
31
+ const unsubscribe = bus.subscribe((envelope: DebugEventEnvelope) => {
32
+ if (request.signal.aborted) return;
33
+ const line = `data: ${JSON.stringify(envelope)}\n\n`;
34
+ writer.write(encoder.encode(line)).catch(() => {
35
+ // Client disconnected, will be cleaned up by abort handler.
36
+ });
37
+ });
38
+
39
+ request.signal.addEventListener("abort", () => {
40
+ unsubscribe();
41
+ writer.close().catch(() => {});
42
+ });
43
+
44
+ return new Response(stream.readable, {
45
+ status: 200,
46
+ headers: {
47
+ "Content-Type": "text/event-stream",
48
+ "Cache-Control": "no-cache",
49
+ Connection: "keep-alive",
50
+ },
51
+ });
52
+ }
@@ -77,6 +77,7 @@ export async function handleRunAgent({
77
77
  request,
78
78
  agent,
79
79
  input,
80
+ agentId,
80
81
  debug: runtime.debug,
81
82
  logger: runtime.debugLogger,
82
83
  });
@@ -1,4 +1,9 @@
1
+ import { BaseEvent, EventType, RunStartedEvent } from "@ag-ui/client";
1
2
  import { CopilotIntelligenceRuntimeLike } from "../../core/runtime";
3
+ import type {
4
+ ConnectThreadBootstrapResponse,
5
+ ConnectThreadLiveResponse,
6
+ } from "../../intelligence-platform/client";
2
7
  import { isPlatformNotFoundError } from "../shared/intelligence-utils";
3
8
  import { resolveIntelligenceUser } from "../shared/resolve-intelligence-user";
4
9
  import { isHandlerResponse } from "../shared/json-response";
@@ -7,13 +12,64 @@ interface HandleIntelligenceConnectParams {
7
12
  runtime: CopilotIntelligenceRuntimeLike;
8
13
  request: Request;
9
14
  threadId: string;
15
+ runId: string;
10
16
  lastSeenEventId: string | null;
11
17
  }
12
18
 
19
+ function stampCanonicalConnectEvent(
20
+ event: BaseEvent,
21
+ threadId: string,
22
+ runId: string,
23
+ ): BaseEvent {
24
+ const {
25
+ thread_id: _threadId,
26
+ run_id: _runId,
27
+ ...eventRecord
28
+ } = event as BaseEvent & {
29
+ thread_id?: unknown;
30
+ run_id?: unknown;
31
+ };
32
+
33
+ if (event.type === EventType.RUN_STARTED) {
34
+ const runStarted = eventRecord as RunStartedEvent;
35
+
36
+ return {
37
+ ...runStarted,
38
+ threadId,
39
+ runId,
40
+ input: {
41
+ ...(runStarted.input ?? {}),
42
+ threadId,
43
+ runId,
44
+ },
45
+ } as RunStartedEvent;
46
+ }
47
+
48
+ return {
49
+ ...eventRecord,
50
+ threadId,
51
+ runId,
52
+ } as BaseEvent;
53
+ }
54
+
55
+ function stampCanonicalConnectPlan(
56
+ result: ConnectThreadBootstrapResponse | ConnectThreadLiveResponse,
57
+ threadId: string,
58
+ runId: string,
59
+ ) {
60
+ return {
61
+ ...result,
62
+ events: result.events.map((event) =>
63
+ stampCanonicalConnectEvent(event, threadId, runId),
64
+ ),
65
+ };
66
+ }
67
+
13
68
  export async function handleIntelligenceConnect({
14
69
  runtime,
15
70
  request,
16
71
  threadId,
72
+ runId,
17
73
  lastSeenEventId,
18
74
  }: HandleIntelligenceConnectParams): Promise<Response> {
19
75
  if (!runtime.intelligence) {
@@ -35,6 +91,7 @@ export async function handleIntelligenceConnect({
35
91
  const result = await runtime.intelligence.ɵconnectThread({
36
92
  threadId,
37
93
  userId: user.id,
94
+ runId,
38
95
  lastSeenEventId,
39
96
  });
40
97
 
@@ -44,7 +101,7 @@ export async function handleIntelligenceConnect({
44
101
  });
45
102
  }
46
103
 
47
- return Response.json(result, {
104
+ return Response.json(stampCanonicalConnectPlan(result, threadId, runId), {
48
105
  headers: { "Cache-Control": "no-cache", Connection: "keep-alive" },
49
106
  });
50
107
  } catch (error) {
@@ -16,8 +16,11 @@ export async function resolveIntelligenceUser(params: {
16
16
  if (!isValidIdentifier(user?.id)) {
17
17
  return errorResponse("identifyUser must return a valid user id", 400);
18
18
  }
19
+ if (typeof user?.name !== "string" || user.name.trim().length === 0) {
20
+ return errorResponse("identifyUser must return a valid user name", 400);
21
+ }
19
22
 
20
- return { id: user.id };
23
+ return { id: user.id, name: user.name };
21
24
  } catch (error) {
22
25
  console.error("Error identifying intelligence user:", error);
23
26
  return errorResponse("Failed to identify user", 500);
@@ -7,12 +7,15 @@ import {
7
7
  type CopilotRuntimeLogger,
8
8
  } from "../../../../lib/logger";
9
9
  import { telemetry } from "../../telemetry";
10
+ import { DebugEventBus } from "../../core/debug-event-bus";
10
11
 
11
12
  interface CreateSseEventResponseParams {
12
13
  request: Request;
13
14
  observableFactory: () =>
14
15
  | Promise<Observable<BaseEvent>>
15
16
  | Observable<BaseEvent>;
17
+ debugEventBus?: DebugEventBus;
18
+ agentId?: string;
16
19
  debug?: ResolvedDebugConfig;
17
20
  /** Pre-created logger instance to avoid creating a new pino logger per request. */
18
21
  logger?: CopilotRuntimeLogger;
@@ -21,6 +24,8 @@ interface CreateSseEventResponseParams {
21
24
  export function createSseEventResponse({
22
25
  request,
23
26
  observableFactory,
27
+ debugEventBus,
28
+ agentId,
24
29
  debug,
25
30
  logger,
26
31
  }: CreateSseEventResponseParams): Response {
@@ -28,6 +33,8 @@ export function createSseEventResponse({
28
33
  const writer = stream.writable.getWriter();
29
34
  const encoder = new EventEncoder();
30
35
  let streamClosed = false;
36
+ let debugThreadId = "";
37
+ let debugRunId = "";
31
38
 
32
39
  const debugLogger = debug?.enabled
33
40
  ? (logger ??
@@ -74,6 +81,37 @@ export function createSseEventResponse({
74
81
 
75
82
  subscription = observable.subscribe({
76
83
  next: async (event) => {
84
+ // Extract threadId/runId from RUN_STARTED
85
+ if (event.type === "RUN_STARTED") {
86
+ const e = event as { threadId?: string; runId?: string };
87
+ debugThreadId = e.threadId ?? "";
88
+ debugRunId = e.runId ?? "";
89
+ }
90
+
91
+ // Broadcast to debug listeners BEFORE the stream-closed gate below.
92
+ // Intentional: debug subscribers (e.g. the VS Code Inspector panel)
93
+ // should still receive trailing events after the SSE client for
94
+ // this request closed its connection — they're independent
95
+ // consumers observing the underlying runtime, not the request's
96
+ // response stream.
97
+ //
98
+ // Wrapped in try/catch so a buggy debug subscriber can't propagate
99
+ // an exception into this observer — if the throw reached the
100
+ // `next` callback it would get routed to `error` by RxJS, closing
101
+ // the SSE stream for an unrelated reason. Log via `logError` and
102
+ // move on.
103
+ if (debugEventBus) {
104
+ try {
105
+ debugEventBus.broadcast(event, {
106
+ agentId: agentId ?? "",
107
+ threadId: debugThreadId,
108
+ runId: debugRunId,
109
+ });
110
+ } catch (broadcastError) {
111
+ logError(broadcastError);
112
+ }
113
+ }
114
+
77
115
  if (!request.signal.aborted && !streamClosed) {
78
116
  try {
79
117
  eventCount++;
@@ -92,6 +130,14 @@ export function createSseEventResponse({
92
130
  } catch (error) {
93
131
  if (error instanceof Error && error.name === "AbortError") {
94
132
  streamClosed = true;
133
+ } else {
134
+ // Non-abort write failures (backpressure disconnects,
135
+ // transform-stream exceptions, …) were previously swallowed
136
+ // silently — `streamClosed` stayed `false` and the next
137
+ // event re-attempted a broken writer. Log and mark the
138
+ // stream closed so we stop trying.
139
+ logError(error);
140
+ streamClosed = true;
95
141
  }
96
142
  }
97
143
  }
@@ -0,0 +1,71 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { of } from "rxjs";
3
+ import { EventType, type BaseEvent } from "@ag-ui/client";
4
+
5
+ vi.mock("pino", () => ({
6
+ default: vi.fn(() => ({
7
+ child: vi.fn(() => ({ debug: vi.fn() })),
8
+ debug: vi.fn(),
9
+ })),
10
+ }));
11
+ vi.mock("pino-pretty", () => ({ default: vi.fn() }));
12
+ vi.mock("../../../telemetry", () => ({
13
+ telemetry: { capture: vi.fn() },
14
+ }));
15
+
16
+ import { handleSseConnect } from "../connect";
17
+ import { DebugEventBus } from "../../../core/debug-event-bus";
18
+
19
+ /**
20
+ * Regression guard for the agentId forwarding fix. `handleSseConnect` used
21
+ * to hardcode `agentId: "connect"` in every debug envelope emitted on
22
+ * /agent/:agentId/connect; the fix threads the route-resolved agentId
23
+ * through to `createSseEventResponse`. Reverting that change would make
24
+ * this test fail — the /run-based integration coverage wouldn't catch it
25
+ * because /run is a different code path.
26
+ */
27
+ describe("handleSseConnect → debug envelope agentId", () => {
28
+ it("forwards the real agentId into envelopes on /connect", async () => {
29
+ const bus = new DebugEventBus();
30
+ const received: Array<{ agentId: string }> = [];
31
+ bus.subscribe((envelope) => {
32
+ received.push({ agentId: envelope.agentId });
33
+ });
34
+
35
+ const event: BaseEvent = {
36
+ type: EventType.RUN_STARTED,
37
+ threadId: "t-1",
38
+ runId: "r-1",
39
+ } as BaseEvent;
40
+
41
+ const fakeRuntime = {
42
+ debugEventBus: bus,
43
+ runner: {
44
+ connect: () => of(event),
45
+ },
46
+ } as any;
47
+
48
+ const response = handleSseConnect({
49
+ runtime: fakeRuntime,
50
+ request: new Request("http://localhost/agent/weather-agent/connect", {
51
+ method: "POST",
52
+ }),
53
+ agentId: "weather-agent",
54
+ threadId: "t-1",
55
+ });
56
+
57
+ // Drain to let the observable subscription fire.
58
+ const reader = response.body!.getReader();
59
+ while (true) {
60
+ const { done } = await reader.read();
61
+ if (done) break;
62
+ }
63
+
64
+ expect(received.length).toBeGreaterThan(0);
65
+ for (const env of received) {
66
+ // A revert to the pre-fix hardcoded "connect" would fail this
67
+ // positive assertion — no need for a separate not-toBe guard.
68
+ expect(env.agentId).toBe("weather-agent");
69
+ }
70
+ });
71
+ });
@@ -5,16 +5,22 @@ import { extractForwardableHeaders } from "../header-utils";
5
5
  interface HandleSseConnectParams {
6
6
  runtime: CopilotRuntimeLike;
7
7
  request: Request;
8
+ agentId: string;
8
9
  threadId: string;
9
10
  }
10
11
 
11
12
  export function handleSseConnect({
12
13
  runtime,
13
14
  request,
15
+ agentId,
14
16
  threadId,
15
17
  }: HandleSseConnectParams): Response {
16
18
  return createSseEventResponse({
17
19
  request,
20
+ debugEventBus: runtime.debugEventBus,
21
+ // Forward the real agentId so debug envelopes reflect the agent the
22
+ // route resolved to — not the literal string "connect".
23
+ agentId,
18
24
  observableFactory: () =>
19
25
  runtime.runner.connect({
20
26
  threadId,
@@ -9,6 +9,7 @@ interface HandleSseRunParams {
9
9
  request: Request;
10
10
  agent: AbstractAgent;
11
11
  input: RunAgentInput;
12
+ agentId: string;
12
13
  debug?: ResolvedDebugConfig;
13
14
  /** Pre-created logger instance to avoid creating a new pino logger per request. */
14
15
  logger?: CopilotRuntimeLogger;
@@ -19,11 +20,14 @@ export function handleSseRun({
19
20
  request,
20
21
  agent,
21
22
  input,
23
+ agentId,
22
24
  debug,
23
25
  logger,
24
26
  }: HandleSseRunParams): Response {
25
27
  return createSseEventResponse({
26
28
  request,
29
+ debugEventBus: runtime.debugEventBus,
30
+ agentId,
27
31
  debug,
28
32
  logger,
29
33
  observableFactory: () =>
@@ -35,7 +35,6 @@ describe("CopilotKitIntelligence", () => {
35
35
  apiUrl: "https://api.example.com",
36
36
  wsUrl: "wss://ws.example.com/socket",
37
37
  apiKey: "test-key",
38
- organizationId: "org-1",
39
38
  });
40
39
  });
41
40
 
@@ -44,7 +43,6 @@ describe("CopilotKitIntelligence", () => {
44
43
  apiUrl: "https://api.example.com/",
45
44
  wsUrl: "wss://ws.example.com/socket",
46
45
  apiKey: "k",
47
- organizationId: "org-1",
48
46
  });
49
47
  fetchMock.mockReturnValue(jsonResponse({ threads: [], joinCode: "" }));
50
48
  await c.listThreads({ userId: "u", agentId: "a" });
@@ -58,7 +56,6 @@ describe("CopilotKitIntelligence", () => {
58
56
  apiUrl: "https://api.example.com",
59
57
  wsUrl: "wss://ws.example.com",
60
58
  apiKey: "k",
61
- organizationId: "org-1",
62
59
  });
63
60
 
64
61
  expect(c.ɵgetRunnerWsUrl()).toBe("wss://ws.example.com/runner");
@@ -71,7 +68,6 @@ describe("CopilotKitIntelligence", () => {
71
68
  const headers = fetchMock.mock.calls[0][1].headers;
72
69
  expect(headers.Authorization).toBe("Bearer test-key");
73
70
  expect(headers["Content-Type"]).toBe("application/json");
74
- expect(headers["X-Organization-Id"]).toBe("org-1");
75
71
  });
76
72
 
77
73
  it("throws on non-ok response", async () => {
@@ -175,7 +171,6 @@ describe("CopilotKitIntelligence", () => {
175
171
  apiUrl: "https://api.example.com",
176
172
  wsUrl: "wss://ws.example.com/socket",
177
173
  apiKey: "test-key",
178
- organizationId: "org-1",
179
174
  onThreadUpdated,
180
175
  });
181
176
  const thread = { id: "t-1", name: "Renamed" };
@@ -225,7 +220,6 @@ describe("CopilotKitIntelligence", () => {
225
220
  apiUrl: "https://api.example.com",
226
221
  wsUrl: "wss://ws.example.com/socket",
227
222
  apiKey: "test-key",
228
- organizationId: "org-1",
229
223
  onThreadCreated,
230
224
  });
231
225
  const thread = { id: "t-1", name: null };
@@ -312,7 +306,6 @@ describe("CopilotKitIntelligence", () => {
312
306
  apiUrl: "https://api.example.com",
313
307
  wsUrl: "wss://ws.example.com/socket",
314
308
  apiKey: "test-key",
315
- organizationId: "org-1",
316
309
  onThreadUpdated,
317
310
  });
318
311
  const thread = { id: "t-1", name: "Archived", archived: true };
@@ -353,7 +346,6 @@ describe("CopilotKitIntelligence", () => {
353
346
  apiUrl: "https://api.example.com",
354
347
  wsUrl: "wss://ws.example.com/socket",
355
348
  apiKey: "test-key",
356
- organizationId: "org-1",
357
349
  onThreadDeleted,
358
350
  });
359
351
  fetchMock.mockReturnValue(jsonResponse(undefined));
@@ -376,7 +368,6 @@ describe("CopilotKitIntelligence", () => {
376
368
  apiUrl: "https://api.example.com",
377
369
  wsUrl: "wss://ws.example.com/socket",
378
370
  apiKey: "test-key",
379
- organizationId: "org-1",
380
371
  onThreadDeleted: () => {
381
372
  throw new Error("callback exploded");
382
373
  },
@@ -503,7 +494,6 @@ describe("CopilotKitIntelligence", () => {
503
494
  apiUrl: "https://api.example.com",
504
495
  wsUrl: "wss://ws.example.com/socket",
505
496
  apiKey: "test-key",
506
- organizationId: "org-1",
507
497
  onThreadUpdated: configCb,
508
498
  });
509
499
  client.onThreadUpdated(runtimeCb);
@@ -554,6 +544,7 @@ describe("CopilotKitIntelligence", () => {
554
544
  const result = await client.ɵconnectThread({
555
545
  threadId: "t-1",
556
546
  userId: "user-1",
547
+ runId: "run-1",
557
548
  lastSeenEventId: "event-1",
558
549
  });
559
550
 
@@ -563,6 +554,7 @@ describe("CopilotKitIntelligence", () => {
563
554
  expect(opts.method).toBe("POST");
564
555
  expect(JSON.parse(opts.body)).toEqual({
565
556
  userId: "user-1",
557
+ runId: "run-1",
566
558
  lastSeenEventId: "event-1",
567
559
  });
568
560
  });
@@ -571,13 +563,22 @@ describe("CopilotKitIntelligence", () => {
571
563
  const payload = {
572
564
  mode: "bootstrap",
573
565
  latestEventId: "event-2",
574
- events: [{ type: "MESSAGES_SNAPSHOT", messages: [] }],
566
+ events: [
567
+ {
568
+ type: "RUN_STARTED",
569
+ threadId: "t-1",
570
+ run_id: "backend-run-1",
571
+ input: { messages: [] },
572
+ },
573
+ { type: "RUN_FINISHED" },
574
+ ],
575
575
  };
576
576
  fetchMock.mockReturnValue(jsonResponse(payload));
577
577
 
578
578
  const result = await client.ɵconnectThread({
579
579
  threadId: "t-1",
580
580
  userId: "user-1",
581
+ runId: "run-1",
581
582
  lastSeenEventId: "event-1",
582
583
  });
583
584
 
@@ -596,6 +597,7 @@ describe("CopilotKitIntelligence", () => {
596
597
  const result = await client.ɵconnectThread({
597
598
  threadId: "t-1",
598
599
  userId: "user-1",
600
+ runId: "run-1",
599
601
  lastSeenEventId: "event-2",
600
602
  });
601
603