@copilotkit/runtime 1.55.2-next.1 → 1.55.3-canary.1776215089
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/CHANGELOG.md +7 -0
- 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.d.cts +2 -1
- package/dist/index.d.mts +2 -1
- package/dist/lib/index.d.cts +1 -0
- package/dist/lib/index.d.cts.map +1 -1
- package/dist/lib/index.d.mts +1 -0
- package/dist/lib/index.d.mts.map +1 -1
- package/dist/lib/runtime/copilot-runtime.cjs.map +1 -1
- package/dist/lib/runtime/copilot-runtime.d.cts +3 -3
- package/dist/lib/runtime/copilot-runtime.d.cts.map +1 -1
- package/dist/lib/runtime/copilot-runtime.d.mts +3 -3
- package/dist/lib/runtime/copilot-runtime.d.mts.map +1 -1
- package/dist/lib/runtime/copilot-runtime.mjs.map +1 -1
- package/dist/package.cjs +1 -1
- package/dist/package.mjs +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/fetch-handler.cjs +43 -3
- 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 +43 -3
- package/dist/v2/runtime/core/fetch-handler.mjs.map +1 -1
- package/dist/v2/runtime/core/fetch-router.cjs +26 -0
- package/dist/v2/runtime/core/fetch-router.cjs.map +1 -1
- package/dist/v2/runtime/core/fetch-router.mjs +26 -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 +13 -0
- package/dist/v2/runtime/core/hooks.d.cts.map +1 -1
- package/dist/v2/runtime/core/hooks.d.mts +13 -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 +13 -0
- package/dist/v2/runtime/core/runtime.cjs.map +1 -1
- package/dist/v2/runtime/core/runtime.d.cts +43 -3
- package/dist/v2/runtime/core/runtime.d.cts.map +1 -1
- package/dist/v2/runtime/core/runtime.d.mts +43 -3
- package/dist/v2/runtime/core/runtime.d.mts.map +1 -1
- package/dist/v2/runtime/core/runtime.mjs +13 -1
- 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 +1 -1
- package/dist/v2/runtime/handlers/handle-run.cjs.map +1 -1
- package/dist/v2/runtime/handlers/handle-run.mjs +1 -1
- 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/intelligence/threads.cjs +179 -0
- package/dist/v2/runtime/handlers/intelligence/threads.cjs.map +1 -0
- package/dist/v2/runtime/handlers/intelligence/threads.mjs +173 -0
- package/dist/v2/runtime/handlers/intelligence/threads.mjs.map +1 -0
- 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/index.d.cts +1 -1
- package/dist/v2/runtime/index.d.mts +1 -1
- package/package.json +2 -2
- 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/copilot-runtime.ts +6 -1
- package/src/v2/runtime/__tests__/agents-factory.test.ts +136 -0
- package/src/v2/runtime/__tests__/fetch-router.test.ts +76 -0
- package/src/v2/runtime/__tests__/get-runtime-info.test.ts +134 -1
- package/src/v2/runtime/core/fetch-handler.ts +55 -4
- package/src/v2/runtime/core/fetch-router.ts +48 -0
- package/src/v2/runtime/core/hooks.ts +6 -1
- package/src/v2/runtime/core/runtime.ts +63 -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 +1 -1
- package/src/v2/runtime/handlers/handle-stop.ts +2 -1
- package/src/v2/runtime/handlers/handle-threads.ts +1 -0
- package/src/v2/runtime/handlers/intelligence/thread-names.ts +1 -1
- package/src/v2/runtime/handlers/intelligence/threads.ts +28 -0
- package/src/v2/runtime/handlers/shared/agent-utils.ts +3 -2
|
@@ -11,24 +11,32 @@ import {
|
|
|
11
11
|
} from "@ag-ui/client";
|
|
12
12
|
import { randomUUID } from "@copilotkit/shared";
|
|
13
13
|
|
|
14
|
+
type ContentPartSource =
|
|
15
|
+
| { type: "data"; value: string; mimeType: string }
|
|
16
|
+
| { type: "url"; value: string; mimeType?: string };
|
|
17
|
+
|
|
14
18
|
/**
|
|
15
19
|
* A TanStack AI content part (text, image, audio, video, or document).
|
|
16
20
|
*/
|
|
17
21
|
export type TanStackContentPart =
|
|
18
22
|
| { type: "text"; content: string }
|
|
19
|
-
| {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
| { type: "url"; value: string; mimeType?: string };
|
|
24
|
-
};
|
|
23
|
+
| { type: "image"; source: ContentPartSource }
|
|
24
|
+
| { type: "audio"; source: ContentPartSource }
|
|
25
|
+
| { type: "video"; source: ContentPartSource }
|
|
26
|
+
| { type: "document"; source: ContentPartSource };
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
29
|
* Message format expected by TanStack AI's `chat()`.
|
|
30
|
+
*
|
|
31
|
+
* Content is typed as `any[]` for the multimodal case so messages are directly
|
|
32
|
+
* passable to any adapter without casts — different adapters constrain which
|
|
33
|
+
* modalities they accept (e.g. OpenAI only allows text + image).
|
|
34
|
+
* Use `TanStackContentPart` to inspect individual parts if needed.
|
|
28
35
|
*/
|
|
29
36
|
export interface TanStackChatMessage {
|
|
30
37
|
role: "user" | "assistant" | "tool";
|
|
31
|
-
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
|
+
content: string | null | any[];
|
|
32
40
|
name?: string;
|
|
33
41
|
toolCalls?: Array<{
|
|
34
42
|
id: string;
|
package/src/agent/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
StateSnapshotEvent,
|
|
21
21
|
StateDeltaEvent,
|
|
22
22
|
} from "@ag-ui/client";
|
|
23
|
+
import type { AgentCapabilities } from "@ag-ui/core";
|
|
23
24
|
import {
|
|
24
25
|
streamText,
|
|
25
26
|
LanguageModel,
|
|
@@ -817,6 +818,15 @@ export interface BuiltInAgentClassicConfig {
|
|
|
817
818
|
* Example: `{ openai: { reasoningEffort: "high" } }`
|
|
818
819
|
*/
|
|
819
820
|
providerOptions?: Record<string, any>;
|
|
821
|
+
/**
|
|
822
|
+
* Explicit agent capabilities. **Shallow-merged** at the category level on
|
|
823
|
+
* top of auto-inferred defaults — providing a category (e.g. `tools`)
|
|
824
|
+
* replaces that entire category, not individual fields within it.
|
|
825
|
+
*
|
|
826
|
+
* For example, `{ tools: { supported: true } }` will drop the inferred
|
|
827
|
+
* `clientProvided` value. Include all fields for any category you override.
|
|
828
|
+
*/
|
|
829
|
+
capabilities?: Partial<AgentCapabilities>;
|
|
820
830
|
}
|
|
821
831
|
|
|
822
832
|
/**
|
|
@@ -854,6 +864,29 @@ export class BuiltInAgent extends AbstractAgent {
|
|
|
854
864
|
return this.config?.overridableProperties?.includes(property) ?? false;
|
|
855
865
|
}
|
|
856
866
|
|
|
867
|
+
async getCapabilities(): Promise<AgentCapabilities> {
|
|
868
|
+
const inferred: AgentCapabilities = {
|
|
869
|
+
tools: {
|
|
870
|
+
supported: true,
|
|
871
|
+
clientProvided: true,
|
|
872
|
+
},
|
|
873
|
+
transport: {
|
|
874
|
+
streaming: true,
|
|
875
|
+
},
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
if (!this.config.capabilities) {
|
|
879
|
+
return inferred;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Shallow merge at the category level — explicit overrides replace
|
|
883
|
+
// entire categories when provided, inferred defaults fill the rest.
|
|
884
|
+
return {
|
|
885
|
+
...inferred,
|
|
886
|
+
...this.config.capabilities,
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
|
|
857
890
|
run(input: RunAgentInput): Observable<BaseEvent> {
|
|
858
891
|
if (isFactoryConfig(this.config)) {
|
|
859
892
|
return this.runFactory(input, this.config);
|
|
@@ -35,8 +35,13 @@ import {
|
|
|
35
35
|
type CopilotRuntimeOptions,
|
|
36
36
|
type CopilotRuntimeOptions as CopilotRuntimeOptionsVNext,
|
|
37
37
|
type AgentRunner,
|
|
38
|
+
type AgentsConfig,
|
|
39
|
+
type AgentsFactory,
|
|
40
|
+
type AgentFactoryContext,
|
|
38
41
|
InMemoryAgentRunner,
|
|
39
42
|
} from "../../v2/runtime";
|
|
43
|
+
|
|
44
|
+
export type { AgentsConfig, AgentsFactory, AgentFactoryContext };
|
|
40
45
|
import { TelemetryAgentRunner } from "./telemetry-agent-runner";
|
|
41
46
|
import telemetry from "../telemetry-client";
|
|
42
47
|
|
|
@@ -318,7 +323,7 @@ interface CopilotRuntimeConstructorParams<T extends Parameter[] | [] = []>
|
|
|
318
323
|
* – the `MaybePromise<NonEmptyRecord<T>>` constraint in `CopilotRuntimeOptionsVNext`
|
|
319
324
|
* – the `Record<string, AbstractAgent>` constraint in `both
|
|
320
325
|
*/
|
|
321
|
-
agents?:
|
|
326
|
+
agents?: AgentsConfig;
|
|
322
327
|
}
|
|
323
328
|
|
|
324
329
|
/**
|
|
@@ -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
|
+
});
|
|
@@ -50,6 +50,57 @@ describe("fetch-router", () => {
|
|
|
50
50
|
expect(result).toBeNull();
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
it("matches GET /threads", () => {
|
|
54
|
+
const result = matchRoute("/api/copilotkit/threads", basePath);
|
|
55
|
+
expect(result).toEqual({ method: "threads/list" });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("matches POST /threads/subscribe", () => {
|
|
59
|
+
const result = matchRoute("/api/copilotkit/threads/subscribe", basePath);
|
|
60
|
+
expect(result).toEqual({ method: "threads/subscribe" });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("matches PATCH /threads/:threadId", () => {
|
|
64
|
+
const result = matchRoute("/api/copilotkit/threads/thread-abc", basePath);
|
|
65
|
+
expect(result).toEqual({
|
|
66
|
+
method: "threads/update",
|
|
67
|
+
threadId: "thread-abc",
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("matches POST /threads/:threadId/archive", () => {
|
|
72
|
+
const result = matchRoute(
|
|
73
|
+
"/api/copilotkit/threads/thread-abc/archive",
|
|
74
|
+
basePath,
|
|
75
|
+
);
|
|
76
|
+
expect(result).toEqual({
|
|
77
|
+
method: "threads/archive",
|
|
78
|
+
threadId: "thread-abc",
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("matches GET /threads/:threadId/messages", () => {
|
|
83
|
+
const result = matchRoute(
|
|
84
|
+
"/api/copilotkit/threads/thread-abc/messages",
|
|
85
|
+
basePath,
|
|
86
|
+
);
|
|
87
|
+
expect(result).toEqual({
|
|
88
|
+
method: "threads/messages",
|
|
89
|
+
threadId: "thread-abc",
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("handles URL-encoded threadId in thread routes", () => {
|
|
94
|
+
const result = matchRoute(
|
|
95
|
+
"/api/copilotkit/threads/thread%2F123",
|
|
96
|
+
basePath,
|
|
97
|
+
);
|
|
98
|
+
expect(result).toEqual({
|
|
99
|
+
method: "threads/update",
|
|
100
|
+
threadId: "thread/123",
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
53
104
|
it("returns null when basePath is a prefix but not a segment boundary", () => {
|
|
54
105
|
const result = matchRoute("/api/copilotkitextra/info", basePath);
|
|
55
106
|
expect(result).toBeNull();
|
|
@@ -124,6 +175,31 @@ describe("fetch-router", () => {
|
|
|
124
175
|
expect(result).toBeNull();
|
|
125
176
|
});
|
|
126
177
|
|
|
178
|
+
it("matches /threads suffix", () => {
|
|
179
|
+
const result = matchRoute("/anything/threads");
|
|
180
|
+
expect(result).toEqual({ method: "threads/list" });
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("matches /threads/subscribe suffix", () => {
|
|
184
|
+
const result = matchRoute("/anything/threads/subscribe");
|
|
185
|
+
expect(result).toEqual({ method: "threads/subscribe" });
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("matches /threads/:threadId suffix", () => {
|
|
189
|
+
const result = matchRoute("/anything/threads/t1");
|
|
190
|
+
expect(result).toEqual({ method: "threads/update", threadId: "t1" });
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("matches /threads/:threadId/archive suffix", () => {
|
|
194
|
+
const result = matchRoute("/anything/threads/t1/archive");
|
|
195
|
+
expect(result).toEqual({ method: "threads/archive", threadId: "t1" });
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("matches /threads/:threadId/messages suffix", () => {
|
|
199
|
+
const result = matchRoute("/anything/threads/t1/messages");
|
|
200
|
+
expect(result).toEqual({ method: "threads/messages", threadId: "t1" });
|
|
201
|
+
});
|
|
202
|
+
|
|
127
203
|
it("works with deeply nested mount prefix", () => {
|
|
128
204
|
const result = matchRoute("/api/v2/copilotkit/agent/a1/run");
|
|
129
205
|
expect(result).toEqual({ method: "agent/run", agentId: "a1" });
|
|
@@ -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> {
|
|
@@ -46,6 +46,14 @@ import { handleConnectAgent } from "../handlers/handle-connect";
|
|
|
46
46
|
import { handleStopAgent } from "../handlers/handle-stop";
|
|
47
47
|
import { handleGetRuntimeInfo } from "../handlers/get-runtime-info";
|
|
48
48
|
import { handleTranscribe } from "../handlers/handle-transcribe";
|
|
49
|
+
import {
|
|
50
|
+
handleListThreads,
|
|
51
|
+
handleSubscribeToThreads,
|
|
52
|
+
handleUpdateThread,
|
|
53
|
+
handleArchiveThread,
|
|
54
|
+
handleDeleteThread,
|
|
55
|
+
handleGetThreadMessages,
|
|
56
|
+
} from "../handlers/handle-threads";
|
|
49
57
|
import {
|
|
50
58
|
parseMethodCall,
|
|
51
59
|
createJsonRequest,
|
|
@@ -306,6 +314,31 @@ function dispatchRoute(
|
|
|
306
314
|
return handleGetRuntimeInfo({ runtime, request });
|
|
307
315
|
case "transcribe":
|
|
308
316
|
return handleTranscribe({ runtime, request });
|
|
317
|
+
case "threads/list":
|
|
318
|
+
return handleListThreads({ runtime, request });
|
|
319
|
+
case "threads/subscribe":
|
|
320
|
+
return handleSubscribeToThreads({ runtime, request });
|
|
321
|
+
case "threads/update":
|
|
322
|
+
if (request.method.toUpperCase() === "DELETE") {
|
|
323
|
+
return handleDeleteThread({
|
|
324
|
+
runtime,
|
|
325
|
+
request,
|
|
326
|
+
threadId: route.threadId,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
return handleUpdateThread({ runtime, request, threadId: route.threadId });
|
|
330
|
+
case "threads/archive":
|
|
331
|
+
return handleArchiveThread({
|
|
332
|
+
runtime,
|
|
333
|
+
request,
|
|
334
|
+
threadId: route.threadId,
|
|
335
|
+
});
|
|
336
|
+
case "threads/messages":
|
|
337
|
+
return handleGetThreadMessages({
|
|
338
|
+
runtime,
|
|
339
|
+
request,
|
|
340
|
+
threadId: route.threadId,
|
|
341
|
+
});
|
|
309
342
|
}
|
|
310
343
|
}
|
|
311
344
|
|
|
@@ -376,10 +409,28 @@ function validateHttpMethod(
|
|
|
376
409
|
route: RouteInfo,
|
|
377
410
|
): Response | null {
|
|
378
411
|
const method = httpMethod.toUpperCase();
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
412
|
+
|
|
413
|
+
switch (route.method) {
|
|
414
|
+
case "info":
|
|
415
|
+
case "threads/list":
|
|
416
|
+
case "threads/messages":
|
|
417
|
+
if (method === "GET") return null;
|
|
418
|
+
return jsonResponse({ error: "Method not allowed" }, 405, {
|
|
419
|
+
Allow: "GET",
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
case "threads/update":
|
|
423
|
+
if (method === "PATCH" || method === "DELETE") return null;
|
|
424
|
+
return jsonResponse({ error: "Method not allowed" }, 405, {
|
|
425
|
+
Allow: "PATCH, DELETE",
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
default:
|
|
429
|
+
if (method === "POST") return null;
|
|
430
|
+
return jsonResponse({ error: "Method not allowed" }, 405, {
|
|
431
|
+
Allow: "POST",
|
|
432
|
+
});
|
|
433
|
+
}
|
|
383
434
|
}
|
|
384
435
|
|
|
385
436
|
/* ------------------------------------------------------------------------------------------------
|
|
@@ -108,5 +108,53 @@ function matchSegments(path: string): RouteInfo | null {
|
|
|
108
108
|
return { method: "agent/stop", agentId, threadId };
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
// /threads/subscribe (2 segments)
|
|
112
|
+
if (
|
|
113
|
+
len >= 2 &&
|
|
114
|
+
segments[len - 2] === "threads" &&
|
|
115
|
+
segments[len - 1] === "subscribe"
|
|
116
|
+
) {
|
|
117
|
+
return { method: "threads/subscribe" };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// /threads/:threadId/messages (3 segments)
|
|
121
|
+
if (
|
|
122
|
+
len >= 3 &&
|
|
123
|
+
segments[len - 3] === "threads" &&
|
|
124
|
+
segments[len - 1] === "messages"
|
|
125
|
+
) {
|
|
126
|
+
const threadId = safeDecodeURIComponent(segments[len - 2]!);
|
|
127
|
+
if (!threadId) return null;
|
|
128
|
+
return { method: "threads/messages", threadId };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// /threads/:threadId/archive (3 segments)
|
|
132
|
+
if (
|
|
133
|
+
len >= 3 &&
|
|
134
|
+
segments[len - 3] === "threads" &&
|
|
135
|
+
segments[len - 1] === "archive"
|
|
136
|
+
) {
|
|
137
|
+
const threadId = safeDecodeURIComponent(segments[len - 2]!);
|
|
138
|
+
if (!threadId) return null;
|
|
139
|
+
return { method: "threads/archive", threadId };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// /threads/:threadId (2 segments) — update or delete
|
|
143
|
+
if (
|
|
144
|
+
len >= 2 &&
|
|
145
|
+
segments[len - 2] === "threads" &&
|
|
146
|
+
segments[len - 1] !== "subscribe"
|
|
147
|
+
) {
|
|
148
|
+
const threadId = safeDecodeURIComponent(segments[len - 1]!);
|
|
149
|
+
if (!threadId) return null;
|
|
150
|
+
// Disambiguated by HTTP method in the handler
|
|
151
|
+
return { method: "threads/update", threadId };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// /threads (1 segment) — list
|
|
155
|
+
if (len >= 1 && segments[len - 1] === "threads") {
|
|
156
|
+
return { method: "threads/list" };
|
|
157
|
+
}
|
|
158
|
+
|
|
111
159
|
return null;
|
|
112
160
|
}
|
|
@@ -38,7 +38,12 @@ export type RouteInfo =
|
|
|
38
38
|
| { method: "agent/connect"; agentId: string }
|
|
39
39
|
| { method: "agent/stop"; agentId: string; threadId: string }
|
|
40
40
|
| { method: "info" }
|
|
41
|
-
| { method: "transcribe" }
|
|
41
|
+
| { method: "transcribe" }
|
|
42
|
+
| { method: "threads/list" }
|
|
43
|
+
| { method: "threads/subscribe" }
|
|
44
|
+
| { method: "threads/update"; threadId: string }
|
|
45
|
+
| { method: "threads/archive"; threadId: string }
|
|
46
|
+
| { method: "threads/messages"; threadId: string };
|
|
42
47
|
|
|
43
48
|
/* ------------------------------------------------------------------------------------------------
|
|
44
49
|
* Hook contexts
|