@copilotkit/runtime 1.55.3 → 1.56.1
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/agent/converters/tanstack.cjs.map +1 -1
- package/dist/agent/converters/tanstack.d.cts +6 -19
- package/dist/agent/converters/tanstack.d.cts.map +1 -1
- package/dist/agent/converters/tanstack.d.mts +6 -19
- package/dist/agent/converters/tanstack.d.mts.map +1 -1
- package/dist/agent/converters/tanstack.mjs.map +1 -1
- package/dist/agent/index.cjs +16 -2
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts +12 -1
- package/dist/agent/index.d.cts.map +1 -1
- package/dist/agent/index.d.mts +12 -1
- package/dist/agent/index.d.mts.map +1 -1
- package/dist/agent/index.mjs +16 -2
- package/dist/agent/index.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +3 -2
- package/dist/index.d.mts +3 -2
- package/dist/index.mjs +1 -1
- package/dist/lib/index.cjs +1 -1
- package/dist/lib/index.d.cts +2 -1
- package/dist/lib/index.d.cts.map +1 -1
- package/dist/lib/index.d.mts +2 -1
- package/dist/lib/index.d.mts.map +1 -1
- package/dist/lib/index.mjs +1 -1
- package/dist/lib/integrations/node-http/index.cjs +4 -1
- package/dist/lib/integrations/node-http/index.cjs.map +1 -1
- package/dist/lib/integrations/node-http/index.d.cts.map +1 -1
- package/dist/lib/integrations/node-http/index.d.mts.map +1 -1
- package/dist/lib/integrations/node-http/index.mjs +4 -1
- package/dist/lib/integrations/node-http/index.mjs.map +1 -1
- package/dist/lib/integrations/shared.cjs +1 -1
- package/dist/lib/integrations/shared.d.cts +1 -1
- package/dist/lib/integrations/shared.d.mts +1 -1
- package/dist/lib/integrations/shared.mjs +1 -1
- package/dist/lib/runtime/copilot-runtime.cjs +25 -5
- package/dist/lib/runtime/copilot-runtime.cjs.map +1 -1
- package/dist/lib/runtime/copilot-runtime.d.cts +15 -3
- package/dist/lib/runtime/copilot-runtime.d.cts.map +1 -1
- package/dist/lib/runtime/copilot-runtime.d.mts +15 -3
- package/dist/lib/runtime/copilot-runtime.d.mts.map +1 -1
- package/dist/lib/runtime/copilot-runtime.mjs +25 -5
- package/dist/lib/runtime/copilot-runtime.mjs.map +1 -1
- package/dist/lib/runtime/mcp-tools-utils.cjs +21 -4
- package/dist/lib/runtime/mcp-tools-utils.cjs.map +1 -1
- package/dist/lib/runtime/mcp-tools-utils.d.cts.map +1 -1
- package/dist/lib/runtime/mcp-tools-utils.d.mts.map +1 -1
- package/dist/lib/runtime/mcp-tools-utils.mjs +21 -4
- package/dist/lib/runtime/mcp-tools-utils.mjs.map +1 -1
- package/dist/package.cjs +6 -5
- package/dist/package.mjs +6 -5
- package/dist/service-adapters/anthropic/anthropic-adapter.cjs +11 -3
- package/dist/service-adapters/anthropic/anthropic-adapter.cjs.map +1 -1
- package/dist/service-adapters/anthropic/anthropic-adapter.d.cts +6 -0
- package/dist/service-adapters/anthropic/anthropic-adapter.d.cts.map +1 -1
- package/dist/service-adapters/anthropic/anthropic-adapter.d.mts +6 -0
- package/dist/service-adapters/anthropic/anthropic-adapter.d.mts.map +1 -1
- package/dist/service-adapters/anthropic/anthropic-adapter.mjs +11 -3
- package/dist/service-adapters/anthropic/anthropic-adapter.mjs.map +1 -1
- package/dist/service-adapters/anthropic/utils.cjs +27 -1
- package/dist/service-adapters/anthropic/utils.cjs.map +1 -1
- package/dist/service-adapters/anthropic/utils.mjs +27 -1
- package/dist/service-adapters/anthropic/utils.mjs.map +1 -1
- package/dist/service-adapters/langchain/utils.cjs +1 -1
- package/dist/service-adapters/langchain/utils.cjs.map +1 -1
- package/dist/service-adapters/langchain/utils.mjs +1 -1
- package/dist/service-adapters/langchain/utils.mjs.map +1 -1
- package/dist/service-adapters/openai/openai-adapter.cjs +3 -2
- package/dist/service-adapters/openai/openai-adapter.cjs.map +1 -1
- package/dist/service-adapters/openai/openai-adapter.d.cts +6 -0
- package/dist/service-adapters/openai/openai-adapter.d.cts.map +1 -1
- package/dist/service-adapters/openai/openai-adapter.d.mts +6 -0
- package/dist/service-adapters/openai/openai-adapter.d.mts.map +1 -1
- package/dist/service-adapters/openai/openai-adapter.mjs +4 -3
- package/dist/service-adapters/openai/openai-adapter.mjs.map +1 -1
- package/dist/service-adapters/openai/openai-assistant-adapter.cjs +8 -9
- package/dist/service-adapters/openai/openai-assistant-adapter.cjs.map +1 -1
- package/dist/service-adapters/openai/openai-assistant-adapter.d.cts.map +1 -1
- package/dist/service-adapters/openai/openai-assistant-adapter.d.mts.map +1 -1
- package/dist/service-adapters/openai/openai-assistant-adapter.mjs +9 -10
- package/dist/service-adapters/openai/openai-assistant-adapter.mjs.map +1 -1
- package/dist/service-adapters/openai/utils.cjs +53 -0
- package/dist/service-adapters/openai/utils.cjs.map +1 -1
- package/dist/service-adapters/openai/utils.mjs +51 -1
- package/dist/service-adapters/openai/utils.mjs.map +1 -1
- package/dist/v2/index.cjs +1 -0
- package/dist/v2/index.d.cts +3 -3
- package/dist/v2/index.d.mts +3 -3
- package/dist/v2/index.mjs +2 -2
- package/dist/v2/runtime/core/middleware-sse-parser.cjs +5 -2
- package/dist/v2/runtime/core/middleware-sse-parser.cjs.map +1 -1
- package/dist/v2/runtime/core/middleware-sse-parser.mjs +5 -2
- package/dist/v2/runtime/core/middleware-sse-parser.mjs.map +1 -1
- package/dist/v2/runtime/core/runtime.cjs +25 -0
- package/dist/v2/runtime/core/runtime.cjs.map +1 -1
- package/dist/v2/runtime/core/runtime.d.cts +53 -4
- package/dist/v2/runtime/core/runtime.d.cts.map +1 -1
- package/dist/v2/runtime/core/runtime.d.mts +53 -4
- package/dist/v2/runtime/core/runtime.d.mts.map +1 -1
- package/dist/v2/runtime/core/runtime.mjs +26 -2
- package/dist/v2/runtime/core/runtime.mjs.map +1 -1
- package/dist/v2/runtime/handlers/get-runtime-info.cjs +18 -10
- package/dist/v2/runtime/handlers/get-runtime-info.cjs.map +1 -1
- package/dist/v2/runtime/handlers/get-runtime-info.mjs +19 -11
- package/dist/v2/runtime/handlers/get-runtime-info.mjs.map +1 -1
- package/dist/v2/runtime/handlers/handle-connect.cjs +1 -1
- package/dist/v2/runtime/handlers/handle-connect.cjs.map +1 -1
- package/dist/v2/runtime/handlers/handle-connect.mjs +1 -1
- package/dist/v2/runtime/handlers/handle-connect.mjs.map +1 -1
- package/dist/v2/runtime/handlers/handle-run.cjs +8 -2
- package/dist/v2/runtime/handlers/handle-run.cjs.map +1 -1
- package/dist/v2/runtime/handlers/handle-run.mjs +8 -2
- package/dist/v2/runtime/handlers/handle-run.mjs.map +1 -1
- package/dist/v2/runtime/handlers/handle-stop.cjs +2 -1
- package/dist/v2/runtime/handlers/handle-stop.cjs.map +1 -1
- package/dist/v2/runtime/handlers/handle-stop.mjs +2 -1
- package/dist/v2/runtime/handlers/handle-stop.mjs.map +1 -1
- package/dist/v2/runtime/handlers/intelligence/thread-names.cjs +1 -1
- package/dist/v2/runtime/handlers/intelligence/thread-names.cjs.map +1 -1
- package/dist/v2/runtime/handlers/intelligence/thread-names.mjs +1 -1
- package/dist/v2/runtime/handlers/intelligence/thread-names.mjs.map +1 -1
- package/dist/v2/runtime/handlers/shared/agent-utils.cjs +3 -2
- package/dist/v2/runtime/handlers/shared/agent-utils.cjs.map +1 -1
- package/dist/v2/runtime/handlers/shared/agent-utils.mjs +3 -2
- package/dist/v2/runtime/handlers/shared/agent-utils.mjs.map +1 -1
- package/dist/v2/runtime/handlers/shared/sse-response.cjs +40 -1
- package/dist/v2/runtime/handlers/shared/sse-response.cjs.map +1 -1
- package/dist/v2/runtime/handlers/shared/sse-response.mjs +40 -1
- package/dist/v2/runtime/handlers/shared/sse-response.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/index.d.cts +1 -1
- package/dist/v2/runtime/index.d.mts +1 -1
- package/package.json +7 -6
- package/src/agent/__tests__/capabilities.test.ts +81 -0
- package/src/agent/__tests__/provider-id-collision.test.ts +195 -0
- package/src/agent/converters/tanstack.ts +15 -7
- package/src/agent/index.ts +52 -11
- package/src/lib/integrations/node-http/__tests__/request-duck-type.test.ts +66 -0
- package/src/lib/integrations/node-http/index.ts +15 -1
- package/src/lib/runtime/__tests__/mcp-tools-utils.test.ts +30 -1
- package/src/lib/runtime/__tests__/on-after-request.test.ts +122 -0
- package/src/lib/runtime/__tests__/v1-agent-factory.test.ts +109 -0
- package/src/lib/runtime/copilot-runtime.ts +54 -5
- package/src/lib/runtime/mcp-tools-utils.ts +41 -6
- package/src/service-adapters/anthropic/anthropic-adapter.ts +22 -2
- package/src/service-adapters/anthropic/utils.ts +60 -1
- package/src/service-adapters/langchain/utils.ts +1 -1
- package/src/service-adapters/openai/__tests__/openai-v5-compat.test.ts +177 -0
- package/src/service-adapters/openai/openai-adapter.ts +17 -2
- package/src/service-adapters/openai/openai-assistant-adapter.ts +7 -9
- package/src/service-adapters/openai/utils.ts +100 -0
- package/src/v2/runtime/__tests__/agents-factory.test.ts +136 -0
- package/src/v2/runtime/__tests__/debug-sse-response.test.ts +302 -0
- package/src/v2/runtime/__tests__/get-runtime-info.test.ts +134 -1
- package/src/v2/runtime/__tests__/middleware-sse-parser.test.ts +50 -0
- package/src/v2/runtime/core/middleware-sse-parser.ts +12 -2
- package/src/v2/runtime/core/runtime.ts +90 -2
- package/src/v2/runtime/handlers/get-runtime-info.ts +33 -8
- package/src/v2/runtime/handlers/handle-connect.ts +1 -1
- package/src/v2/runtime/handlers/handle-run.ts +16 -2
- package/src/v2/runtime/handlers/handle-stop.ts +2 -1
- package/src/v2/runtime/handlers/intelligence/thread-names.ts +1 -1
- package/src/v2/runtime/handlers/shared/agent-utils.ts +3 -2
- package/src/v2/runtime/handlers/shared/sse-response.ts +69 -0
- package/src/v2/runtime/handlers/sse/run.ts +9 -0
- package/tests/service-adapters/anthropic/anthropic-adapter.test.ts +268 -0
- package/tests/service-adapters/anthropic/utils-token-trimming.test.ts +301 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { Observable, of } from "rxjs";
|
|
3
|
+
import { BaseEvent, EventType } from "@ag-ui/client";
|
|
4
|
+
import type { ResolvedDebugConfig } from "@copilotkit/shared";
|
|
5
|
+
|
|
6
|
+
const mockDebug = vi.fn();
|
|
7
|
+
|
|
8
|
+
vi.mock("pino", () => ({
|
|
9
|
+
default: vi.fn(() => ({
|
|
10
|
+
child: vi.fn(() => ({ debug: mockDebug })),
|
|
11
|
+
debug: mockDebug,
|
|
12
|
+
})),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock("pino-pretty", () => ({ default: vi.fn() }));
|
|
16
|
+
|
|
17
|
+
vi.mock("../../telemetry", () => ({
|
|
18
|
+
telemetry: { capture: vi.fn() },
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
import pino from "pino";
|
|
22
|
+
import { createSseEventResponse } from "../handlers/shared/sse-response";
|
|
23
|
+
|
|
24
|
+
function createTestObservable(events: BaseEvent[]): Observable<BaseEvent> {
|
|
25
|
+
return of(...events);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function createMockRequest(): Request {
|
|
29
|
+
return new Request("https://example.com/agent/test/run", {
|
|
30
|
+
method: "POST",
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function drainResponse(response: Response): Promise<void> {
|
|
35
|
+
const reader = response.body!.getReader();
|
|
36
|
+
while (true) {
|
|
37
|
+
const { done } = await reader.read();
|
|
38
|
+
if (done) break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("createSseEventResponse debug logging", () => {
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
mockDebug.mockClear();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("does not log when debug is undefined", async () => {
|
|
48
|
+
const response = createSseEventResponse({
|
|
49
|
+
request: createMockRequest(),
|
|
50
|
+
observableFactory: () => createTestObservable([]),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await drainResponse(response);
|
|
54
|
+
|
|
55
|
+
expect(mockDebug).not.toHaveBeenCalled();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("logs lifecycle message on stream open", async () => {
|
|
59
|
+
const debug: ResolvedDebugConfig = {
|
|
60
|
+
enabled: true,
|
|
61
|
+
events: false,
|
|
62
|
+
lifecycle: true,
|
|
63
|
+
verbose: false,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const event: BaseEvent = {
|
|
67
|
+
type: EventType.RUN_STARTED,
|
|
68
|
+
threadId: "t-1",
|
|
69
|
+
runId: "r-1",
|
|
70
|
+
} as BaseEvent;
|
|
71
|
+
|
|
72
|
+
const response = createSseEventResponse({
|
|
73
|
+
request: createMockRequest(),
|
|
74
|
+
observableFactory: () => createTestObservable([event]),
|
|
75
|
+
debug,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
await drainResponse(response);
|
|
79
|
+
|
|
80
|
+
expect(mockDebug).toHaveBeenCalledWith("SSE stream opened");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("logs lifecycle completion with total eventCount even when event logging is off", async () => {
|
|
84
|
+
const debug: ResolvedDebugConfig = {
|
|
85
|
+
enabled: true,
|
|
86
|
+
events: false,
|
|
87
|
+
lifecycle: true,
|
|
88
|
+
verbose: false,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const events: BaseEvent[] = [
|
|
92
|
+
{
|
|
93
|
+
type: EventType.RUN_STARTED,
|
|
94
|
+
threadId: "t-1",
|
|
95
|
+
runId: "r-1",
|
|
96
|
+
} as BaseEvent,
|
|
97
|
+
{
|
|
98
|
+
type: EventType.RUN_FINISHED,
|
|
99
|
+
threadId: "t-1",
|
|
100
|
+
runId: "r-1",
|
|
101
|
+
} as BaseEvent,
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
const response = createSseEventResponse({
|
|
105
|
+
request: createMockRequest(),
|
|
106
|
+
observableFactory: () => createTestObservable(events),
|
|
107
|
+
debug,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await drainResponse(response);
|
|
111
|
+
|
|
112
|
+
expect(mockDebug).toHaveBeenCalledWith(
|
|
113
|
+
{ eventCount: 2, loggedEventCount: 0 },
|
|
114
|
+
"SSE stream completed",
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("logs lifecycle completion with matching eventCount and loggedEventCount when events enabled", async () => {
|
|
119
|
+
const debug: ResolvedDebugConfig = {
|
|
120
|
+
enabled: true,
|
|
121
|
+
events: true,
|
|
122
|
+
lifecycle: true,
|
|
123
|
+
verbose: false,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const events: BaseEvent[] = [
|
|
127
|
+
{
|
|
128
|
+
type: EventType.RUN_STARTED,
|
|
129
|
+
threadId: "t-1",
|
|
130
|
+
runId: "r-1",
|
|
131
|
+
} as BaseEvent,
|
|
132
|
+
{
|
|
133
|
+
type: EventType.RUN_FINISHED,
|
|
134
|
+
threadId: "t-1",
|
|
135
|
+
runId: "r-1",
|
|
136
|
+
} as BaseEvent,
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
const response = createSseEventResponse({
|
|
140
|
+
request: createMockRequest(),
|
|
141
|
+
observableFactory: () => createTestObservable(events),
|
|
142
|
+
debug,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
await drainResponse(response);
|
|
146
|
+
|
|
147
|
+
expect(mockDebug).toHaveBeenCalledWith(
|
|
148
|
+
{ eventCount: 2, loggedEventCount: 2 },
|
|
149
|
+
"SSE stream completed",
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("logs events in summary mode when verbose is false", async () => {
|
|
154
|
+
const debug: ResolvedDebugConfig = {
|
|
155
|
+
enabled: true,
|
|
156
|
+
events: true,
|
|
157
|
+
lifecycle: false,
|
|
158
|
+
verbose: false,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const event: BaseEvent = {
|
|
162
|
+
type: EventType.TEXT_MESSAGE_START,
|
|
163
|
+
messageId: "msg-1",
|
|
164
|
+
role: "assistant",
|
|
165
|
+
} as BaseEvent;
|
|
166
|
+
|
|
167
|
+
const response = createSseEventResponse({
|
|
168
|
+
request: createMockRequest(),
|
|
169
|
+
observableFactory: () => createTestObservable([event]),
|
|
170
|
+
debug,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await drainResponse(response);
|
|
174
|
+
|
|
175
|
+
const eventEmittedCalls = mockDebug.mock.calls.filter(
|
|
176
|
+
(call) => call[call.length - 1] === "Event emitted",
|
|
177
|
+
);
|
|
178
|
+
expect(eventEmittedCalls.length).toBeGreaterThanOrEqual(1);
|
|
179
|
+
|
|
180
|
+
const [summaryArg] = eventEmittedCalls[0];
|
|
181
|
+
expect(summaryArg).toHaveProperty("type", EventType.TEXT_MESSAGE_START);
|
|
182
|
+
expect(summaryArg).toHaveProperty("messageId", "msg-1");
|
|
183
|
+
// In summary mode the full event object is not passed directly
|
|
184
|
+
expect(summaryArg).not.toHaveProperty("event");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("logs events in verbose mode with full event object", async () => {
|
|
188
|
+
const debug: ResolvedDebugConfig = {
|
|
189
|
+
enabled: true,
|
|
190
|
+
events: true,
|
|
191
|
+
lifecycle: false,
|
|
192
|
+
verbose: true,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const event: BaseEvent = {
|
|
196
|
+
type: EventType.TEXT_MESSAGE_START,
|
|
197
|
+
messageId: "msg-1",
|
|
198
|
+
role: "assistant",
|
|
199
|
+
} as BaseEvent;
|
|
200
|
+
|
|
201
|
+
const response = createSseEventResponse({
|
|
202
|
+
request: createMockRequest(),
|
|
203
|
+
observableFactory: () => createTestObservable([event]),
|
|
204
|
+
debug,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
await drainResponse(response);
|
|
208
|
+
|
|
209
|
+
const eventEmittedCalls = mockDebug.mock.calls.filter(
|
|
210
|
+
(call) => call[call.length - 1] === "Event emitted",
|
|
211
|
+
);
|
|
212
|
+
expect(eventEmittedCalls.length).toBeGreaterThanOrEqual(1);
|
|
213
|
+
|
|
214
|
+
const [verboseArg] = eventEmittedCalls[0];
|
|
215
|
+
expect(verboseArg).toHaveProperty("event");
|
|
216
|
+
expect(verboseArg.event).toEqual(event);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("uses a pre-created logger instead of calling createLogger when one is provided", async () => {
|
|
220
|
+
const debug: ResolvedDebugConfig = {
|
|
221
|
+
enabled: true,
|
|
222
|
+
events: false,
|
|
223
|
+
lifecycle: true,
|
|
224
|
+
verbose: false,
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const externalDebug = vi.fn();
|
|
228
|
+
const externalLogger = { debug: externalDebug } as any;
|
|
229
|
+
|
|
230
|
+
// Clear the pino mock call count so we can assert it was NOT called again
|
|
231
|
+
const pinoMock = pino as unknown as ReturnType<typeof vi.fn>;
|
|
232
|
+
pinoMock.mockClear();
|
|
233
|
+
|
|
234
|
+
const event: BaseEvent = {
|
|
235
|
+
type: EventType.RUN_STARTED,
|
|
236
|
+
threadId: "t-1",
|
|
237
|
+
runId: "r-1",
|
|
238
|
+
} as BaseEvent;
|
|
239
|
+
|
|
240
|
+
const response = createSseEventResponse({
|
|
241
|
+
request: createMockRequest(),
|
|
242
|
+
observableFactory: () => createTestObservable([event]),
|
|
243
|
+
debug,
|
|
244
|
+
logger: externalLogger,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
await drainResponse(response);
|
|
248
|
+
|
|
249
|
+
// The external logger should have been used for lifecycle messages
|
|
250
|
+
expect(externalDebug).toHaveBeenCalledWith("SSE stream opened");
|
|
251
|
+
expect(externalDebug).toHaveBeenCalledWith(
|
|
252
|
+
{ eventCount: 1, loggedEventCount: 0 },
|
|
253
|
+
"SSE stream completed",
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
// pino should NOT have been called – the pre-created logger was reused
|
|
257
|
+
expect(pinoMock).not.toHaveBeenCalled();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("does not log events when events is disabled", async () => {
|
|
261
|
+
const debug: ResolvedDebugConfig = {
|
|
262
|
+
enabled: true,
|
|
263
|
+
events: false,
|
|
264
|
+
lifecycle: true,
|
|
265
|
+
verbose: false,
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const events: BaseEvent[] = [
|
|
269
|
+
{
|
|
270
|
+
type: EventType.TEXT_MESSAGE_START,
|
|
271
|
+
messageId: "msg-1",
|
|
272
|
+
role: "assistant",
|
|
273
|
+
} as BaseEvent,
|
|
274
|
+
{
|
|
275
|
+
type: EventType.RUN_FINISHED,
|
|
276
|
+
threadId: "t-1",
|
|
277
|
+
runId: "r-1",
|
|
278
|
+
} as BaseEvent,
|
|
279
|
+
];
|
|
280
|
+
|
|
281
|
+
const response = createSseEventResponse({
|
|
282
|
+
request: createMockRequest(),
|
|
283
|
+
observableFactory: () => createTestObservable(events),
|
|
284
|
+
debug,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
await drainResponse(response);
|
|
288
|
+
|
|
289
|
+
const eventEmittedCalls = mockDebug.mock.calls.filter(
|
|
290
|
+
(call) => call[call.length - 1] === "Event emitted",
|
|
291
|
+
);
|
|
292
|
+
expect(eventEmittedCalls).toHaveLength(0);
|
|
293
|
+
|
|
294
|
+
// Only lifecycle calls should be present
|
|
295
|
+
const lifecycleCalls = mockDebug.mock.calls.filter(
|
|
296
|
+
(call) =>
|
|
297
|
+
call[call.length - 1] === "SSE stream opened" ||
|
|
298
|
+
call[call.length - 1] === "SSE stream completed",
|
|
299
|
+
);
|
|
300
|
+
expect(lifecycleCalls.length).toBeGreaterThanOrEqual(1);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { handleGetRuntimeInfo } from "../handlers/get-runtime-info";
|
|
2
2
|
import { CopilotRuntime } from "../core/runtime";
|
|
3
3
|
import { TranscriptionService } from "../transcription-service/transcription-service";
|
|
4
|
-
import { describe, it, expect } from "vitest";
|
|
4
|
+
import { describe, it, expect, vi } from "vitest";
|
|
5
5
|
import type { AbstractAgent } from "@ag-ui/client";
|
|
6
6
|
|
|
7
7
|
// Mock transcription service
|
|
@@ -117,6 +117,139 @@ describe("handleGetRuntimeInfo", () => {
|
|
|
117
117
|
expect(data.a2uiEnabled).toBe(true);
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
+
it("should include capabilities when agent implements getCapabilities", async () => {
|
|
121
|
+
const mockCapabilities = {
|
|
122
|
+
tools: { supported: true, clientProvided: true },
|
|
123
|
+
transport: { streaming: true },
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const mockAgent = {
|
|
127
|
+
description: "Capable agent",
|
|
128
|
+
constructor: { name: "CapableAgent" },
|
|
129
|
+
getCapabilities: async () => mockCapabilities,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const runtime = new CopilotRuntime({
|
|
133
|
+
agents: {
|
|
134
|
+
capableAgent: mockAgent as unknown as AbstractAgent,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const response = await handleGetRuntimeInfo({
|
|
139
|
+
runtime,
|
|
140
|
+
request: mockRequest,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(response.status).toBe(200);
|
|
144
|
+
|
|
145
|
+
const data = await response.json();
|
|
146
|
+
expect(data.agents.capableAgent.capabilities).toEqual(mockCapabilities);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should omit capabilities when agent does not implement getCapabilities", async () => {
|
|
150
|
+
const mockAgent = {
|
|
151
|
+
description: "Basic agent",
|
|
152
|
+
constructor: { name: "BasicAgent" },
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const runtime = new CopilotRuntime({
|
|
156
|
+
agents: {
|
|
157
|
+
basicAgent: mockAgent as unknown as AbstractAgent,
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const response = await handleGetRuntimeInfo({
|
|
162
|
+
runtime,
|
|
163
|
+
request: mockRequest,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(response.status).toBe(200);
|
|
167
|
+
|
|
168
|
+
const data = await response.json();
|
|
169
|
+
expect(data.agents.basicAgent.capabilities).toBeUndefined();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("should include empty capabilities object when getCapabilities returns {}", async () => {
|
|
173
|
+
const mockAgent = {
|
|
174
|
+
description: "Empty caps agent",
|
|
175
|
+
constructor: { name: "EmptyCapsAgent" },
|
|
176
|
+
getCapabilities: async () => ({}),
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const runtime = new CopilotRuntime({
|
|
180
|
+
agents: {
|
|
181
|
+
emptyAgent: mockAgent as unknown as AbstractAgent,
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const response = await handleGetRuntimeInfo({
|
|
186
|
+
runtime,
|
|
187
|
+
request: mockRequest,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
expect(response.status).toBe(200);
|
|
191
|
+
|
|
192
|
+
const data = await response.json();
|
|
193
|
+
// {} is truthy, so it should be included in the response
|
|
194
|
+
expect(data.agents.emptyAgent.capabilities).toEqual({});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("should isolate per-agent getCapabilities failures and log a warning", async () => {
|
|
198
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
199
|
+
|
|
200
|
+
const failingAgent = {
|
|
201
|
+
description: "Failing agent",
|
|
202
|
+
constructor: { name: "FailingAgent" },
|
|
203
|
+
getCapabilities: async () => {
|
|
204
|
+
throw new Error("capability fetch failed");
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const healthyAgent = {
|
|
209
|
+
description: "Healthy agent",
|
|
210
|
+
constructor: { name: "HealthyAgent" },
|
|
211
|
+
getCapabilities: async () => ({
|
|
212
|
+
tools: { supported: true },
|
|
213
|
+
}),
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const runtime = new CopilotRuntime({
|
|
217
|
+
agents: {
|
|
218
|
+
failing: failingAgent as unknown as AbstractAgent,
|
|
219
|
+
healthy: healthyAgent as unknown as AbstractAgent,
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const response = await handleGetRuntimeInfo({
|
|
224
|
+
runtime,
|
|
225
|
+
request: mockRequest,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
expect(response.status).toBe(200);
|
|
229
|
+
|
|
230
|
+
const data = await response.json();
|
|
231
|
+
// Failing agent should still appear but without capabilities
|
|
232
|
+
expect(data.agents.failing).toEqual({
|
|
233
|
+
name: "failing",
|
|
234
|
+
description: "Failing agent",
|
|
235
|
+
className: "FailingAgent",
|
|
236
|
+
});
|
|
237
|
+
expect(data.agents.failing.capabilities).toBeUndefined();
|
|
238
|
+
|
|
239
|
+
// Healthy agent should have its capabilities
|
|
240
|
+
expect(data.agents.healthy.capabilities).toEqual({
|
|
241
|
+
tools: { supported: true },
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Error should be logged, not silently swallowed
|
|
245
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
246
|
+
'Failed to fetch capabilities for agent "failing":',
|
|
247
|
+
"capability fetch failed",
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
warnSpy.mockRestore();
|
|
251
|
+
});
|
|
252
|
+
|
|
120
253
|
it("should return 500 error when runtime.agents throws an error", async () => {
|
|
121
254
|
const runtime = {
|
|
122
255
|
get agents(): Record<string, AbstractAgent> {
|
|
@@ -83,6 +83,56 @@ describe("parseSSEResponse", () => {
|
|
|
83
83
|
});
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
+
it("normalizes array content in TOOL_CALL_RESULT (MCP adapters)", async () => {
|
|
87
|
+
const response = buildSSEResponse([
|
|
88
|
+
{ type: "RUN_STARTED", threadId: "t-1", runId: "r-1" },
|
|
89
|
+
{
|
|
90
|
+
type: "TOOL_CALL_RESULT",
|
|
91
|
+
toolCallId: "tc-1",
|
|
92
|
+
messageId: "m-result",
|
|
93
|
+
role: "tool",
|
|
94
|
+
content: [
|
|
95
|
+
{ type: "text", text: '{"metric":"cpu","value":42}' },
|
|
96
|
+
{ type: "text", text: " extra info" },
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
{ type: "RUN_FINISHED", threadId: "t-1", runId: "r-1" },
|
|
100
|
+
]);
|
|
101
|
+
const result = await parseSSEResponse(response);
|
|
102
|
+
expect(result.messages).toContainEqual({
|
|
103
|
+
id: "m-result",
|
|
104
|
+
role: "tool",
|
|
105
|
+
content: '{"metric":"cpu","value":42} extra info',
|
|
106
|
+
toolCallId: "tc-1",
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("filters non-text parts when normalizing array content in TOOL_CALL_RESULT", async () => {
|
|
111
|
+
const response = buildSSEResponse([
|
|
112
|
+
{ type: "RUN_STARTED", threadId: "t-1", runId: "r-1" },
|
|
113
|
+
{
|
|
114
|
+
type: "TOOL_CALL_RESULT",
|
|
115
|
+
toolCallId: "tc-1",
|
|
116
|
+
messageId: "m-result",
|
|
117
|
+
role: "tool",
|
|
118
|
+
content: [
|
|
119
|
+
{ type: "text", text: "valid" },
|
|
120
|
+
{ type: "image", data: "binary" },
|
|
121
|
+
null,
|
|
122
|
+
{ type: "text", text: " part" },
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
{ type: "RUN_FINISHED", threadId: "t-1", runId: "r-1" },
|
|
126
|
+
]);
|
|
127
|
+
const result = await parseSSEResponse(response);
|
|
128
|
+
expect(result.messages).toContainEqual({
|
|
129
|
+
id: "m-result",
|
|
130
|
+
role: "tool",
|
|
131
|
+
content: "valid part",
|
|
132
|
+
toolCallId: "tc-1",
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
86
136
|
it("uses MESSAGES_SNAPSHOT when present", async () => {
|
|
87
137
|
const snapshotMessages = [
|
|
88
138
|
{ id: "u-1", role: "user", content: "hi" },
|
|
@@ -167,14 +167,24 @@ export async function parseSSEResponse(
|
|
|
167
167
|
break;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
case "TOOL_CALL_RESULT":
|
|
170
|
+
case "TOOL_CALL_RESULT": {
|
|
171
|
+
// langchain-mcp-adapters may send content as an array of
|
|
172
|
+
// {type:"text", text:string} objects instead of a plain string.
|
|
173
|
+
let resultContent = event.content;
|
|
174
|
+
if (Array.isArray(resultContent)) {
|
|
175
|
+
resultContent = resultContent
|
|
176
|
+
.filter((part: any) => part && typeof part.text === "string")
|
|
177
|
+
.map((part: any) => part.text)
|
|
178
|
+
.join("");
|
|
179
|
+
}
|
|
171
180
|
messagesById.set(event.messageId, {
|
|
172
181
|
id: event.messageId,
|
|
173
182
|
role: "tool",
|
|
174
|
-
content:
|
|
183
|
+
content: resultContent,
|
|
175
184
|
toolCallId: event.toolCallId,
|
|
176
185
|
});
|
|
177
186
|
break;
|
|
187
|
+
}
|
|
178
188
|
}
|
|
179
189
|
}
|
|
180
190
|
|
|
@@ -9,6 +9,11 @@ import {
|
|
|
9
9
|
createLicenseChecker,
|
|
10
10
|
type LicenseChecker,
|
|
11
11
|
} from "@copilotkit/license-verifier";
|
|
12
|
+
import {
|
|
13
|
+
type ResolvedDebugConfig,
|
|
14
|
+
resolveDebugConfig,
|
|
15
|
+
type DebugConfig,
|
|
16
|
+
} from "@copilotkit/shared";
|
|
12
17
|
import { AbstractAgent } from "@ag-ui/client";
|
|
13
18
|
import type { MCPClientConfig } from "@ag-ui/mcp-apps-middleware";
|
|
14
19
|
import { A2UIMiddlewareConfig } from "@ag-ui/a2ui-middleware";
|
|
@@ -17,6 +22,7 @@ import type {
|
|
|
17
22
|
BeforeRequestMiddleware,
|
|
18
23
|
AfterRequestMiddleware,
|
|
19
24
|
} from "./middleware";
|
|
25
|
+
import { createLogger, type CopilotRuntimeLogger } from "../../../lib/logger";
|
|
20
26
|
import { TranscriptionService } from "../transcription-service/transcription-service";
|
|
21
27
|
import { AgentRunner } from "../runner/agent-runner";
|
|
22
28
|
import { InMemoryAgentRunner } from "../runner/in-memory";
|
|
@@ -56,9 +62,70 @@ interface CopilotRuntimeMiddlewares {
|
|
|
56
62
|
openGenerativeUI?: OpenGenerativeUIConfig;
|
|
57
63
|
}
|
|
58
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Context passed to agent factory functions for per-request agent resolution.
|
|
67
|
+
*/
|
|
68
|
+
export interface AgentFactoryContext {
|
|
69
|
+
/** The incoming HTTP request. */
|
|
70
|
+
request: Request;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A function that dynamically creates agents on a per-request basis.
|
|
75
|
+
* Useful for multi-tenant scenarios or request-scoped agent configuration.
|
|
76
|
+
*/
|
|
77
|
+
export type AgentsFactory = (
|
|
78
|
+
ctx: AgentFactoryContext,
|
|
79
|
+
) => MaybePromise<NonEmptyRecord<Record<string, AbstractAgent>>>;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Agents can be provided as:
|
|
83
|
+
* - A static record of agents
|
|
84
|
+
* - A Promise that resolves to a record of agents
|
|
85
|
+
* - A factory function that receives request context and returns agents (or a Promise of agents)
|
|
86
|
+
*/
|
|
87
|
+
export type AgentsConfig =
|
|
88
|
+
| MaybePromise<NonEmptyRecord<Record<string, AbstractAgent>>>
|
|
89
|
+
| AgentsFactory;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Resolve an AgentsConfig value to a concrete record of agents.
|
|
93
|
+
* If the config is a factory function, it is called with the given request context.
|
|
94
|
+
* Otherwise it is awaited directly (static record or Promise).
|
|
95
|
+
*/
|
|
96
|
+
export async function resolveAgents(
|
|
97
|
+
agents: AgentsConfig,
|
|
98
|
+
request?: Request,
|
|
99
|
+
): Promise<Record<string, AbstractAgent>> {
|
|
100
|
+
if (typeof agents === "function") {
|
|
101
|
+
if (!request) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
"Agent factory function requires a request context, but none was provided.",
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
return agents({ request });
|
|
107
|
+
}
|
|
108
|
+
return agents;
|
|
109
|
+
}
|
|
110
|
+
|
|
59
111
|
interface BaseCopilotRuntimeOptions extends CopilotRuntimeMiddlewares {
|
|
60
|
-
/**
|
|
61
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Map of available agents, or a factory function for per-request agent resolution.
|
|
114
|
+
*
|
|
115
|
+
* Static record:
|
|
116
|
+
* ```ts
|
|
117
|
+
* agents: { support: new SupportAgent(), technical: new TechnicalAgent() }
|
|
118
|
+
* ```
|
|
119
|
+
*
|
|
120
|
+
* Factory function (called per-request):
|
|
121
|
+
* ```ts
|
|
122
|
+
* agents: ({ request }) => {
|
|
123
|
+
* const tenantId = request.headers.get("x-tenant-id");
|
|
124
|
+
* return { default: createAgentForTenant(tenantId) };
|
|
125
|
+
* }
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
agents: AgentsConfig;
|
|
62
129
|
/** Optional transcription service for audio processing. */
|
|
63
130
|
transcriptionService?: TranscriptionService;
|
|
64
131
|
/** Optional *before* middleware – callback function or webhook URL. */
|
|
@@ -67,6 +134,8 @@ interface BaseCopilotRuntimeOptions extends CopilotRuntimeMiddlewares {
|
|
|
67
134
|
afterRequestMiddleware?: AfterRequestMiddleware;
|
|
68
135
|
/** Signed license token for server-side feature verification. Falls back to COPILOTKIT_LICENSE_TOKEN env var. */
|
|
69
136
|
licenseToken?: string;
|
|
137
|
+
/** Enable debug logging for the event pipeline. */
|
|
138
|
+
debug?: DebugConfig;
|
|
70
139
|
}
|
|
71
140
|
|
|
72
141
|
export interface CopilotRuntimeUser {
|
|
@@ -120,6 +189,8 @@ export interface CopilotRuntimeLike {
|
|
|
120
189
|
identifyUser?: IdentifyUserCallback;
|
|
121
190
|
mode: RuntimeMode;
|
|
122
191
|
licenseChecker?: LicenseChecker;
|
|
192
|
+
debug: ResolvedDebugConfig;
|
|
193
|
+
debugLogger?: CopilotRuntimeLogger;
|
|
123
194
|
}
|
|
124
195
|
|
|
125
196
|
export interface CopilotSseRuntimeLike extends CopilotRuntimeLike {
|
|
@@ -147,6 +218,8 @@ abstract class BaseCopilotRuntime implements CopilotRuntimeLike {
|
|
|
147
218
|
public mcpApps: CopilotRuntimeOptions["mcpApps"];
|
|
148
219
|
public openGenerativeUI: CopilotRuntimeOptions["openGenerativeUI"];
|
|
149
220
|
public licenseChecker?: LicenseChecker;
|
|
221
|
+
public debug: ResolvedDebugConfig;
|
|
222
|
+
public debugLogger?: CopilotRuntimeLogger;
|
|
150
223
|
|
|
151
224
|
abstract readonly intelligence?: CopilotKitIntelligence;
|
|
152
225
|
abstract readonly mode: RuntimeMode;
|
|
@@ -170,6 +243,13 @@ abstract class BaseCopilotRuntime implements CopilotRuntimeLike {
|
|
|
170
243
|
this.mcpApps = mcpApps;
|
|
171
244
|
this.openGenerativeUI = openGenerativeUI;
|
|
172
245
|
this.runner = runner;
|
|
246
|
+
this.debug = resolveDebugConfig(options.debug);
|
|
247
|
+
if (this.debug.enabled) {
|
|
248
|
+
this.debugLogger = createLogger({
|
|
249
|
+
level: "debug",
|
|
250
|
+
component: "copilotkit-debug",
|
|
251
|
+
});
|
|
252
|
+
}
|
|
173
253
|
}
|
|
174
254
|
}
|
|
175
255
|
|
|
@@ -326,4 +406,12 @@ export class CopilotRuntime implements CopilotRuntimeLike {
|
|
|
326
406
|
get licenseChecker() {
|
|
327
407
|
return this.delegate.licenseChecker;
|
|
328
408
|
}
|
|
409
|
+
|
|
410
|
+
get debug(): ResolvedDebugConfig {
|
|
411
|
+
return this.delegate.debug;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
get debugLogger(): CopilotRuntimeLogger | undefined {
|
|
415
|
+
return this.delegate.debugLogger;
|
|
416
|
+
}
|
|
329
417
|
}
|