@copilotkit/runtime 1.56.1 → 1.56.2-canary.test-welcome-screen
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/graphql/resolvers/copilot.resolver.cjs +2 -1
- package/dist/graphql/resolvers/copilot.resolver.cjs.map +1 -1
- package/dist/graphql/resolvers/copilot.resolver.mjs +2 -1
- package/dist/graphql/resolvers/copilot.resolver.mjs.map +1 -1
- package/dist/graphql/resolvers/resolve-message-id.cjs +19 -0
- package/dist/graphql/resolvers/resolve-message-id.cjs.map +1 -0
- package/dist/graphql/resolvers/resolve-message-id.mjs +18 -0
- package/dist/graphql/resolvers/resolve-message-id.mjs.map +1 -0
- package/dist/lib/runtime/copilot-runtime.cjs +4 -2
- package/dist/lib/runtime/copilot-runtime.cjs.map +1 -1
- package/dist/lib/runtime/copilot-runtime.d.cts.map +1 -1
- package/dist/lib/runtime/copilot-runtime.d.mts.map +1 -1
- package/dist/lib/runtime/copilot-runtime.mjs +4 -2
- package/dist/lib/runtime/copilot-runtime.mjs.map +1 -1
- package/dist/package.cjs +2 -2
- package/dist/package.mjs +2 -2
- package/dist/v2/runtime/core/debug-event-bus.cjs +36 -0
- package/dist/v2/runtime/core/debug-event-bus.cjs.map +1 -0
- package/dist/v2/runtime/core/debug-event-bus.d.cts +19 -0
- package/dist/v2/runtime/core/debug-event-bus.d.cts.map +1 -0
- package/dist/v2/runtime/core/debug-event-bus.d.mts +19 -0
- package/dist/v2/runtime/core/debug-event-bus.d.mts.map +1 -0
- package/dist/v2/runtime/core/debug-event-bus.mjs +35 -0
- package/dist/v2/runtime/core/debug-event-bus.mjs.map +1 -0
- package/dist/v2/runtime/core/fetch-handler.cjs +6 -0
- package/dist/v2/runtime/core/fetch-handler.cjs.map +1 -1
- package/dist/v2/runtime/core/fetch-handler.d.cts.map +1 -1
- package/dist/v2/runtime/core/fetch-handler.d.mts.map +1 -1
- package/dist/v2/runtime/core/fetch-handler.mjs +6 -0
- package/dist/v2/runtime/core/fetch-handler.mjs.map +1 -1
- package/dist/v2/runtime/core/fetch-router.cjs +1 -0
- package/dist/v2/runtime/core/fetch-router.cjs.map +1 -1
- package/dist/v2/runtime/core/fetch-router.mjs +1 -0
- package/dist/v2/runtime/core/fetch-router.mjs.map +1 -1
- package/dist/v2/runtime/core/hooks.cjs.map +1 -1
- package/dist/v2/runtime/core/hooks.d.cts +2 -0
- package/dist/v2/runtime/core/hooks.d.cts.map +1 -1
- package/dist/v2/runtime/core/hooks.d.mts +2 -0
- package/dist/v2/runtime/core/hooks.d.mts.map +1 -1
- package/dist/v2/runtime/core/hooks.mjs.map +1 -1
- package/dist/v2/runtime/core/runtime.cjs +5 -0
- package/dist/v2/runtime/core/runtime.cjs.map +1 -1
- package/dist/v2/runtime/core/runtime.d.cts +5 -0
- package/dist/v2/runtime/core/runtime.d.cts.map +1 -1
- package/dist/v2/runtime/core/runtime.d.mts +5 -0
- package/dist/v2/runtime/core/runtime.d.mts.map +1 -1
- package/dist/v2/runtime/core/runtime.mjs +5 -0
- package/dist/v2/runtime/core/runtime.mjs.map +1 -1
- package/dist/v2/runtime/handlers/handle-connect.cjs +2 -0
- package/dist/v2/runtime/handlers/handle-connect.cjs.map +1 -1
- package/dist/v2/runtime/handlers/handle-connect.mjs +2 -0
- package/dist/v2/runtime/handlers/handle-connect.mjs.map +1 -1
- package/dist/v2/runtime/handlers/handle-debug-events.cjs +33 -0
- package/dist/v2/runtime/handlers/handle-debug-events.cjs.map +1 -0
- package/dist/v2/runtime/handlers/handle-debug-events.mjs +32 -0
- package/dist/v2/runtime/handlers/handle-debug-events.mjs.map +1 -0
- package/dist/v2/runtime/handlers/handle-run.cjs +1 -0
- package/dist/v2/runtime/handlers/handle-run.cjs.map +1 -1
- package/dist/v2/runtime/handlers/handle-run.mjs +1 -0
- package/dist/v2/runtime/handlers/handle-run.mjs.map +1 -1
- package/dist/v2/runtime/handlers/intelligence/connect.cjs +32 -2
- package/dist/v2/runtime/handlers/intelligence/connect.cjs.map +1 -1
- package/dist/v2/runtime/handlers/intelligence/connect.mjs +31 -2
- package/dist/v2/runtime/handlers/intelligence/connect.mjs.map +1 -1
- package/dist/v2/runtime/handlers/shared/resolve-intelligence-user.cjs +5 -1
- package/dist/v2/runtime/handlers/shared/resolve-intelligence-user.cjs.map +1 -1
- package/dist/v2/runtime/handlers/shared/resolve-intelligence-user.mjs +5 -1
- package/dist/v2/runtime/handlers/shared/resolve-intelligence-user.mjs.map +1 -1
- package/dist/v2/runtime/handlers/shared/sse-response.cjs +21 -1
- package/dist/v2/runtime/handlers/shared/sse-response.cjs.map +1 -1
- package/dist/v2/runtime/handlers/shared/sse-response.mjs +21 -1
- package/dist/v2/runtime/handlers/shared/sse-response.mjs.map +1 -1
- package/dist/v2/runtime/handlers/sse/connect.cjs +3 -1
- package/dist/v2/runtime/handlers/sse/connect.cjs.map +1 -1
- package/dist/v2/runtime/handlers/sse/connect.mjs +3 -1
- package/dist/v2/runtime/handlers/sse/connect.mjs.map +1 -1
- package/dist/v2/runtime/handlers/sse/run.cjs +3 -1
- package/dist/v2/runtime/handlers/sse/run.cjs.map +1 -1
- package/dist/v2/runtime/handlers/sse/run.mjs +3 -1
- package/dist/v2/runtime/handlers/sse/run.mjs.map +1 -1
- package/dist/v2/runtime/intelligence-platform/client.cjs +2 -7
- package/dist/v2/runtime/intelligence-platform/client.cjs.map +1 -1
- package/dist/v2/runtime/intelligence-platform/client.d.cts +1 -4
- package/dist/v2/runtime/intelligence-platform/client.d.cts.map +1 -1
- package/dist/v2/runtime/intelligence-platform/client.d.mts +1 -4
- package/dist/v2/runtime/intelligence-platform/client.d.mts.map +1 -1
- package/dist/v2/runtime/intelligence-platform/client.mjs +2 -7
- package/dist/v2/runtime/intelligence-platform/client.mjs.map +1 -1
- package/dist/v2/runtime/runner/intelligence.cjs +17 -5
- package/dist/v2/runtime/runner/intelligence.cjs.map +1 -1
- package/dist/v2/runtime/runner/intelligence.d.cts +1 -0
- package/dist/v2/runtime/runner/intelligence.d.cts.map +1 -1
- package/dist/v2/runtime/runner/intelligence.d.mts +1 -0
- package/dist/v2/runtime/runner/intelligence.d.mts.map +1 -1
- package/dist/v2/runtime/runner/intelligence.mjs +17 -5
- package/dist/v2/runtime/runner/intelligence.mjs.map +1 -1
- package/package.json +3 -3
- package/src/agents/langgraph/__tests__/event-source.test.ts +256 -0
- package/src/graphql/resolvers/__tests__/resolve-message-id.test.ts +25 -0
- package/src/graphql/resolvers/copilot.resolver.ts +2 -1
- package/src/graphql/resolvers/resolve-message-id.ts +14 -0
- package/src/lib/runtime/__tests__/handle-service-adapter.test.ts +108 -0
- package/src/lib/runtime/__tests__/retry-utils.test.ts +137 -0
- package/src/lib/runtime/agent-integrations/langgraph/__tests__/dispatch-event-filtering.test.ts +190 -0
- package/src/lib/runtime/copilot-runtime.ts +20 -4
- package/src/lib/runtime/retry-utils.ts +41 -1
- package/src/v2/runtime/__tests__/fetch-router.test.ts +22 -0
- package/src/v2/runtime/__tests__/handle-connect.test.ts +58 -5
- package/src/v2/runtime/__tests__/handle-run.test.ts +31 -4
- package/src/v2/runtime/__tests__/handle-threads.test.ts +66 -4
- package/src/v2/runtime/__tests__/integration/node-servers.integration.test.ts +19 -0
- package/src/v2/runtime/__tests__/integration/suites/debug-events.suite.ts +253 -0
- package/src/v2/runtime/__tests__/runtime.test.ts +3 -1
- package/src/v2/runtime/core/__tests__/debug-event-bus.test.ts +156 -0
- package/src/v2/runtime/core/debug-event-bus.ts +45 -0
- package/src/v2/runtime/core/fetch-handler.ts +4 -0
- package/src/v2/runtime/core/fetch-router.ts +11 -0
- package/src/v2/runtime/core/hooks.ts +2 -1
- package/src/v2/runtime/core/runtime.ts +12 -0
- package/src/v2/runtime/handlers/__tests__/handle-debug-events.test.ts +176 -0
- package/src/v2/runtime/handlers/handle-connect.ts +2 -0
- package/src/v2/runtime/handlers/handle-debug-events.ts +52 -0
- package/src/v2/runtime/handlers/handle-run.ts +1 -0
- package/src/v2/runtime/handlers/intelligence/connect.ts +58 -1
- package/src/v2/runtime/handlers/shared/resolve-intelligence-user.ts +4 -1
- package/src/v2/runtime/handlers/shared/sse-response.ts +46 -0
- package/src/v2/runtime/handlers/sse/__tests__/sse-connect-agent-id.test.ts +71 -0
- package/src/v2/runtime/handlers/sse/connect.ts +6 -0
- package/src/v2/runtime/handlers/sse/run.ts +4 -0
- package/src/v2/runtime/intelligence-platform/__tests__/client.test.ts +13 -11
- package/src/v2/runtime/intelligence-platform/client.ts +3 -11
- package/src/v2/runtime/runner/__tests__/intelligence-runner.test.ts +51 -1
- 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
|
+
}
|
|
@@ -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: [
|
|
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
|
|