@copilotkit/runtime 1.55.3 → 1.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +14 -0
- 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 +14 -0
- 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/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 +14 -4
- 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 +14 -4
- package/dist/lib/runtime/copilot-runtime.mjs.map +1 -1
- package/dist/package.cjs +6 -5
- package/dist/package.mjs +6 -5
- package/dist/service-adapters/openai/openai-adapter.cjs +1 -1
- package/dist/service-adapters/openai/openai-adapter.cjs.map +1 -1
- package/dist/service-adapters/openai/openai-adapter.d.cts.map +1 -1
- package/dist/service-adapters/openai/openai-adapter.d.mts.map +1 -1
- package/dist/service-adapters/openai/openai-adapter.mjs +2 -2
- 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/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/converters/tanstack.ts +15 -7
- package/src/agent/index.ts +33 -0
- package/src/lib/runtime/__tests__/v1-agent-factory.test.ts +109 -0
- package/src/lib/runtime/copilot-runtime.ts +38 -2
- package/src/service-adapters/openai/__tests__/openai-v5-compat.test.ts +177 -0
- package/src/service-adapters/openai/openai-adapter.ts +3 -1
- 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/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
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { HttpAgent } from "@ag-ui/client";
|
|
3
|
+
import {
|
|
4
|
+
resolveAgents,
|
|
5
|
+
type AgentsConfig,
|
|
6
|
+
type AgentFactoryContext,
|
|
7
|
+
} from "../core/runtime";
|
|
8
|
+
import { handleRunAgent } from "../handlers/handle-run";
|
|
9
|
+
import { handleGetRuntimeInfo } from "../handlers/get-runtime-info";
|
|
10
|
+
import { CopilotRuntime } from "../core/runtime";
|
|
11
|
+
|
|
12
|
+
function createMockAgent(name = "test") {
|
|
13
|
+
return new HttpAgent({ url: `https://example.com/${name}` });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function createMockRuntime(agents: AgentsConfig) {
|
|
17
|
+
return {
|
|
18
|
+
agents,
|
|
19
|
+
transcriptionService: undefined,
|
|
20
|
+
beforeRequestMiddleware: undefined,
|
|
21
|
+
afterRequestMiddleware: undefined,
|
|
22
|
+
runner: { stop: vi.fn().mockResolvedValue(true) },
|
|
23
|
+
mode: "sse",
|
|
24
|
+
} as unknown as CopilotRuntime;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function createMockRequest(headers?: Record<string, string>) {
|
|
28
|
+
return new Request("https://example.com/agent/test/run", {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
31
|
+
body: JSON.stringify({ threadId: "thread-1", messages: [], state: {} }),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("resolveAgents", () => {
|
|
36
|
+
it("resolves a static record", async () => {
|
|
37
|
+
const agents = { default: createMockAgent() };
|
|
38
|
+
const result = await resolveAgents(agents);
|
|
39
|
+
expect(result).toBe(agents);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("resolves a Promise", async () => {
|
|
43
|
+
const agents = { default: createMockAgent() };
|
|
44
|
+
const result = await resolveAgents(Promise.resolve(agents));
|
|
45
|
+
expect(result).toEqual(agents);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("calls a factory function with request context", async () => {
|
|
49
|
+
const agent = createMockAgent();
|
|
50
|
+
const factory = vi.fn().mockReturnValue({ default: agent });
|
|
51
|
+
const request = createMockRequest({ "x-tenant-id": "tenant-123" });
|
|
52
|
+
|
|
53
|
+
const result = await resolveAgents(factory, request);
|
|
54
|
+
|
|
55
|
+
expect(factory).toHaveBeenCalledTimes(1);
|
|
56
|
+
expect(factory).toHaveBeenCalledWith({ request });
|
|
57
|
+
expect(result).toEqual({ default: agent });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("calls an async factory function", async () => {
|
|
61
|
+
const agent = createMockAgent();
|
|
62
|
+
const factory = vi.fn().mockResolvedValue({ default: agent });
|
|
63
|
+
const request = createMockRequest();
|
|
64
|
+
|
|
65
|
+
const result = await resolveAgents(factory, request);
|
|
66
|
+
expect(factory).toHaveBeenCalledTimes(1);
|
|
67
|
+
expect(result).toEqual({ default: agent });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("throws when factory is used without a request", async () => {
|
|
71
|
+
const factory = vi.fn().mockReturnValue({ default: createMockAgent() });
|
|
72
|
+
|
|
73
|
+
await expect(resolveAgents(factory)).rejects.toThrow(
|
|
74
|
+
"Agent factory function requires a request context",
|
|
75
|
+
);
|
|
76
|
+
expect(factory).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("factory can read request headers for per-tenant resolution", async () => {
|
|
80
|
+
const tenantAgentA = createMockAgent("tenant-a");
|
|
81
|
+
const tenantAgentB = createMockAgent("tenant-b");
|
|
82
|
+
|
|
83
|
+
const factory = ({ request }: AgentFactoryContext) => {
|
|
84
|
+
const tenantId = request.headers.get("x-tenant-id");
|
|
85
|
+
if (tenantId === "a") return { default: tenantAgentA };
|
|
86
|
+
return { default: tenantAgentB };
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const requestA = createMockRequest({ "x-tenant-id": "a" });
|
|
90
|
+
const requestB = createMockRequest({ "x-tenant-id": "b" });
|
|
91
|
+
|
|
92
|
+
const resultA = await resolveAgents(factory, requestA);
|
|
93
|
+
expect(resultA.default).toBe(tenantAgentA);
|
|
94
|
+
|
|
95
|
+
const resultB = await resolveAgents(factory, requestB);
|
|
96
|
+
expect(resultB.default).toBe(tenantAgentB);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("handleRunAgent with agents factory", () => {
|
|
101
|
+
it("returns 404 when factory returns no matching agent", async () => {
|
|
102
|
+
const factory = vi.fn().mockReturnValue({
|
|
103
|
+
other: createMockAgent("other"),
|
|
104
|
+
});
|
|
105
|
+
const runtime = createMockRuntime(factory);
|
|
106
|
+
const request = createMockRequest();
|
|
107
|
+
|
|
108
|
+
const response = await handleRunAgent({
|
|
109
|
+
runtime,
|
|
110
|
+
request,
|
|
111
|
+
agentId: "nonexistent",
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(response.status).toBe(404);
|
|
115
|
+
expect(factory).toHaveBeenCalledWith({ request });
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("handleGetRuntimeInfo with agents factory", () => {
|
|
120
|
+
it("resolves factory and lists agents", async () => {
|
|
121
|
+
const factory = vi.fn().mockReturnValue({
|
|
122
|
+
support: createMockAgent("support"),
|
|
123
|
+
technical: createMockAgent("technical"),
|
|
124
|
+
});
|
|
125
|
+
const runtime = createMockRuntime(factory);
|
|
126
|
+
const request = createMockRequest();
|
|
127
|
+
|
|
128
|
+
const response = await handleGetRuntimeInfo({ runtime, request });
|
|
129
|
+
|
|
130
|
+
expect(response.status).toBe(200);
|
|
131
|
+
const body = await response.json();
|
|
132
|
+
expect(Object.keys(body.agents)).toContain("support");
|
|
133
|
+
expect(Object.keys(body.agents)).toContain("technical");
|
|
134
|
+
expect(factory).toHaveBeenCalledWith({ request });
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -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> {
|