@copilotkitnext/core 0.0.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.
Files changed (63) hide show
  1. package/.turbo/turbo-build.log +22 -0
  2. package/.turbo/turbo-check-types.log +4 -0
  3. package/.turbo/turbo-lint.log +12 -0
  4. package/.turbo/turbo-test.log +96 -0
  5. package/LICENSE +11 -0
  6. package/coverage/base.css +224 -0
  7. package/coverage/block-navigation.js +87 -0
  8. package/coverage/favicon.png +0 -0
  9. package/coverage/index.html +131 -0
  10. package/coverage/lcov-report/base.css +224 -0
  11. package/coverage/lcov-report/block-navigation.js +87 -0
  12. package/coverage/lcov-report/favicon.png +0 -0
  13. package/coverage/lcov-report/index.html +131 -0
  14. package/coverage/lcov-report/prettify.css +1 -0
  15. package/coverage/lcov-report/prettify.js +2 -0
  16. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  17. package/coverage/lcov-report/sorter.js +210 -0
  18. package/coverage/lcov-report/src/agent.ts.html +193 -0
  19. package/coverage/lcov-report/src/core.ts.html +919 -0
  20. package/coverage/lcov-report/src/index.html +146 -0
  21. package/coverage/lcov-report/src/types.ts.html +112 -0
  22. package/coverage/lcov-report/src/utils/index.html +116 -0
  23. package/coverage/lcov-report/src/utils/markdown.ts.html +895 -0
  24. package/coverage/lcov.info +556 -0
  25. package/coverage/prettify.css +1 -0
  26. package/coverage/prettify.js +2 -0
  27. package/coverage/sort-arrow-sprite.png +0 -0
  28. package/coverage/sorter.js +210 -0
  29. package/coverage/src/agent.ts.html +193 -0
  30. package/coverage/src/core.ts.html +919 -0
  31. package/coverage/src/index.html +146 -0
  32. package/coverage/src/types.ts.html +112 -0
  33. package/coverage/src/utils/index.html +116 -0
  34. package/coverage/src/utils/markdown.ts.html +895 -0
  35. package/dist/index.d.mts +93 -0
  36. package/dist/index.d.ts +93 -0
  37. package/dist/index.js +526 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/index.mjs +502 -0
  40. package/dist/index.mjs.map +1 -0
  41. package/eslint.config.mjs +3 -0
  42. package/package.json +46 -0
  43. package/src/__tests__/core-agent-constraints.test.ts +86 -0
  44. package/src/__tests__/core-basic-functionality.test.ts +158 -0
  45. package/src/__tests__/core-basic.test.ts +27 -0
  46. package/src/__tests__/core-edge-cases.test.ts +166 -0
  47. package/src/__tests__/core-follow-up.test.ts +216 -0
  48. package/src/__tests__/core-full.test.ts +320 -0
  49. package/src/__tests__/core-simple.test.ts +24 -0
  50. package/src/__tests__/core-tool-minimal.test.ts +41 -0
  51. package/src/__tests__/core-tool-simple.test.ts +40 -0
  52. package/src/__tests__/core-wildcard.test.ts +45 -0
  53. package/src/__tests__/import.test.ts +13 -0
  54. package/src/__tests__/simple.test.ts +7 -0
  55. package/src/__tests__/test-utils.ts +162 -0
  56. package/src/agent.ts +37 -0
  57. package/src/core.ts +336 -0
  58. package/src/index.ts +4 -0
  59. package/src/types.ts +23 -0
  60. package/src/utils/markdown.ts +271 -0
  61. package/tsconfig.json +12 -0
  62. package/tsup.config.ts +11 -0
  63. package/vitest.config.mjs +15 -0
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@copilotkitnext/core",
3
+ "version": "0.0.1",
4
+ "description": "Core web utilities for CopilotKit2",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.mjs",
11
+ "require": "./dist/index.js"
12
+ }
13
+ },
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^22.15.3",
19
+ "@vitest/coverage-v8": "^3.2.4",
20
+ "eslint": "^9.30.0",
21
+ "tsup": "^8.5.0",
22
+ "typescript": "5.8.2",
23
+ "vitest": "^3.2.4",
24
+ "@copilotkitnext/typescript-config": "0.0.0",
25
+ "@copilotkitnext/eslint-config": "0.0.0"
26
+ },
27
+ "dependencies": {
28
+ "@ag-ui/client": "0.0.36-alpha.1",
29
+ "rxjs": "7.8.1",
30
+ "zod": "^3.25.75",
31
+ "@copilotkitnext/shared": "0.0.1"
32
+ },
33
+ "engines": {
34
+ "node": ">=18"
35
+ },
36
+ "scripts": {
37
+ "build": "tsup",
38
+ "dev": "tsup --watch",
39
+ "lint": "eslint . --max-warnings 0",
40
+ "check-types": "tsc --noEmit",
41
+ "clean": "rm -rf dist",
42
+ "test": "vitest run",
43
+ "test:watch": "vitest",
44
+ "test:coverage": "vitest run --coverage"
45
+ }
46
+ }
@@ -0,0 +1,86 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { CopilotKitCore } from '../core';
3
+ import { FrontendTool } from '../types';
4
+ import { MockAgent, createAssistantMessage } from './test-utils';
5
+
6
+ describe('CopilotKitCore - Agent Constraints', () => {
7
+ it('should add tool with agentId', () => {
8
+ const core = new CopilotKitCore({
9
+ headers: {},
10
+ properties: {},
11
+ });
12
+
13
+ const tool: FrontendTool = {
14
+ name: 'testTool',
15
+ handler: vi.fn(),
16
+ agentId: 'agent1',
17
+ };
18
+
19
+ core.addTool(tool);
20
+ expect(core.tools['testTool']).toBeDefined();
21
+ expect(core.tools['testTool']?.agentId).toBe('agent1');
22
+ });
23
+
24
+ it('should add multiple tools with different agentIds', () => {
25
+ const core = new CopilotKitCore({
26
+ headers: {},
27
+ properties: {},
28
+ });
29
+
30
+ const globalTool: FrontendTool = {
31
+ name: 'globalTool',
32
+ handler: vi.fn(),
33
+ };
34
+
35
+ const agent1Tool: FrontendTool = {
36
+ name: 'agent1Tool',
37
+ handler: vi.fn(),
38
+ agentId: 'agent1',
39
+ };
40
+
41
+ const agent2Tool: FrontendTool = {
42
+ name: 'agent2Tool',
43
+ handler: vi.fn(),
44
+ agentId: 'agent2',
45
+ };
46
+
47
+ core.addTool(globalTool);
48
+ core.addTool(agent1Tool);
49
+ core.addTool(agent2Tool);
50
+
51
+ expect(core.tools['globalTool']).toBeDefined();
52
+ expect(core.tools['globalTool']?.agentId).toBeUndefined();
53
+
54
+ expect(core.tools['agent1Tool']).toBeDefined();
55
+ expect(core.tools['agent1Tool']?.agentId).toBe('agent1');
56
+
57
+ expect(core.tools['agent2Tool']).toBeDefined();
58
+ expect(core.tools['agent2Tool']?.agentId).toBe('agent2');
59
+ });
60
+
61
+ it('should preserve all FrontendTool properties including agentId', () => {
62
+ const core = new CopilotKitCore({
63
+ headers: {},
64
+ properties: {},
65
+ });
66
+
67
+ const handler = vi.fn(async () => 'result');
68
+ const tool: FrontendTool = {
69
+ name: 'fullTool',
70
+ description: 'A complete tool',
71
+ handler,
72
+ followUp: false,
73
+ agentId: 'specificAgent',
74
+ };
75
+
76
+ core.addTool(tool);
77
+
78
+ const addedTool = core.tools['fullTool'];
79
+ expect(addedTool).toBeDefined();
80
+ expect(addedTool?.name).toBe('fullTool');
81
+ expect(addedTool?.description).toBe('A complete tool');
82
+ expect(addedTool?.handler).toBe(handler);
83
+ expect(addedTool?.followUp).toBe(false);
84
+ expect(addedTool?.agentId).toBe('specificAgent');
85
+ });
86
+ });
@@ -0,0 +1,158 @@
1
+ import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
2
+ import { CopilotKitCore } from "../core";
3
+ import {
4
+ MockAgent,
5
+ createMessage,
6
+ createAssistantMessage,
7
+ createToolCallMessage,
8
+ createTool,
9
+ } from "./test-utils";
10
+
11
+ describe("CopilotKitCore.runAgent - Basic Functionality", () => {
12
+ let copilotKitCore: CopilotKitCore;
13
+
14
+ beforeEach(() => {
15
+ copilotKitCore = new CopilotKitCore({});
16
+ vi.clearAllMocks();
17
+ });
18
+
19
+ afterEach(() => {
20
+ vi.restoreAllMocks();
21
+ });
22
+
23
+ it("should run agent without tools", async () => {
24
+ const messages = [
25
+ createMessage({ content: "Hello" }),
26
+ createAssistantMessage({ content: "Hi there!" }),
27
+ ];
28
+ const agent = new MockAgent({ newMessages: messages });
29
+
30
+ const result = await copilotKitCore.runAgent({ agent: agent as any });
31
+
32
+ expect(result.newMessages).toEqual(messages);
33
+ expect(agent.runAgentCalls).toHaveLength(1);
34
+ expect(agent.runAgentCalls[0].forwardedProps).toEqual({});
35
+ });
36
+
37
+ it("should pass withMessages to agent.addMessages", async () => {
38
+ const initialMessages = [createMessage({ content: "Initial" })];
39
+ const newMessages = [createAssistantMessage({ content: "Response" })];
40
+ const agent = new MockAgent({ newMessages });
41
+
42
+ await copilotKitCore.runAgent({ agent: agent as any, withMessages: initialMessages });
43
+
44
+ expect(agent.addMessages).toHaveBeenCalledWith(initialMessages);
45
+ expect(agent.messages).toContain(initialMessages[0]);
46
+ });
47
+
48
+ it("should work without withMessages parameter", async () => {
49
+ const newMessages = [createAssistantMessage({ content: "Response" })];
50
+ const agent = new MockAgent({ newMessages });
51
+
52
+ const result = await copilotKitCore.runAgent({ agent: agent as any });
53
+
54
+ expect(result.newMessages).toEqual(newMessages);
55
+ expect(agent.addMessages).not.toHaveBeenCalled();
56
+ });
57
+
58
+ it("should forward properties to agent.runAgent", async () => {
59
+ const properties = { apiKey: "test-key", model: "gpt-4" };
60
+ copilotKitCore = new CopilotKitCore({ properties });
61
+ const agent = new MockAgent({ newMessages: [] });
62
+
63
+ await copilotKitCore.runAgent({ agent: agent as any });
64
+
65
+ expect(agent.runAgentCalls[0].forwardedProps).toEqual(properties);
66
+ });
67
+
68
+ it("should handle empty newMessages array", async () => {
69
+ const agent = new MockAgent({ newMessages: [] });
70
+
71
+ const result = await copilotKitCore.runAgent({ agent: agent as any });
72
+
73
+ expect(result.newMessages).toEqual([]);
74
+ expect(agent.runAgentCalls).toHaveLength(1);
75
+ });
76
+
77
+ it("should ignore non-assistant messages for tool processing", async () => {
78
+ const messages = [
79
+ createMessage({ role: "user", content: "User message" }),
80
+ createMessage({ role: "system", content: "System message" }),
81
+ createMessage({ role: "tool", content: "Tool result", toolCallId: "123" }),
82
+ ];
83
+ const agent = new MockAgent({ newMessages: messages });
84
+
85
+ const result = await copilotKitCore.runAgent({ agent: agent as any });
86
+
87
+ expect(result.newMessages).toEqual(messages);
88
+ expect(agent.runAgentCalls).toHaveLength(1);
89
+ });
90
+
91
+ it("should handle messages with undefined toolCalls", async () => {
92
+ const message = createAssistantMessage({
93
+ content: "Response",
94
+ toolCalls: undefined
95
+ });
96
+ const agent = new MockAgent({ newMessages: [message] });
97
+
98
+ const result = await copilotKitCore.runAgent({ agent: agent as any });
99
+
100
+ expect(result.newMessages).toEqual([message]);
101
+ expect(agent.runAgentCalls).toHaveLength(1);
102
+ });
103
+
104
+ it("should handle tool returning undefined as empty string", async () => {
105
+ const toolName = "undefinedTool";
106
+ const tool = createTool({
107
+ name: toolName,
108
+ handler: vi.fn(async () => undefined),
109
+ });
110
+ copilotKitCore.addTool(tool);
111
+
112
+ const message = createToolCallMessage(toolName);
113
+ const agent = new MockAgent({ newMessages: [message] });
114
+
115
+ await copilotKitCore.runAgent({ agent: agent as any });
116
+
117
+ expect(agent.messages).toContainEqual(
118
+ expect.objectContaining({
119
+ role: "tool",
120
+ content: "", // Should be empty string, not "undefined"
121
+ })
122
+ );
123
+ });
124
+
125
+ it("should handle tool returning null as empty string", async () => {
126
+ const toolName = "nullTool";
127
+ const tool = createTool({
128
+ name: toolName,
129
+ handler: vi.fn(async () => null),
130
+ });
131
+ copilotKitCore.addTool(tool);
132
+
133
+ const message = createToolCallMessage(toolName);
134
+ const agent = new MockAgent({ newMessages: [message] });
135
+
136
+ await copilotKitCore.runAgent({ agent: agent as any });
137
+
138
+ expect(agent.messages).toContainEqual(
139
+ expect.objectContaining({
140
+ role: "tool",
141
+ content: "", // Should be empty string, not "null"
142
+ })
143
+ );
144
+ });
145
+
146
+ it("should return correct result structure", async () => {
147
+ const newMessages = [
148
+ createAssistantMessage({ content: "Test" })
149
+ ];
150
+ const agent = new MockAgent({ newMessages });
151
+
152
+ const result = await copilotKitCore.runAgent({ agent: agent as any });
153
+
154
+ expect(result).toHaveProperty("newMessages");
155
+ expect(Array.isArray(result.newMessages)).toBe(true);
156
+ expect(result.newMessages).toEqual(newMessages);
157
+ });
158
+ });
@@ -0,0 +1,27 @@
1
+ import { describe, it, expect, beforeEach, vi } from "vitest";
2
+ import { CopilotKitCore } from "../core";
3
+
4
+ describe("CopilotKitCore Basic", () => {
5
+ let copilotKitCore: CopilotKitCore;
6
+
7
+ beforeEach(() => {
8
+ copilotKitCore = new CopilotKitCore({});
9
+ });
10
+
11
+ it("should create an instance", () => {
12
+ expect(copilotKitCore).toBeDefined();
13
+ expect(copilotKitCore.agents).toEqual({});
14
+ expect(copilotKitCore.tools).toEqual({});
15
+ });
16
+
17
+ it("should add a tool", () => {
18
+ const tool = {
19
+ name: "testTool",
20
+ handler: vi.fn(),
21
+ };
22
+
23
+ copilotKitCore.addTool(tool);
24
+
25
+ expect(copilotKitCore.tools["testTool"]).toBe(tool);
26
+ });
27
+ });
@@ -0,0 +1,166 @@
1
+ import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
2
+ import { CopilotKitCore } from "../core";
3
+ import {
4
+ MockAgent,
5
+ createAssistantMessage,
6
+ createToolCallMessage,
7
+ createToolResultMessage,
8
+ createTool,
9
+ } from "./test-utils";
10
+
11
+ describe("CopilotKitCore.runAgent - Edge Cases", () => {
12
+ let copilotKitCore: CopilotKitCore;
13
+
14
+ beforeEach(() => {
15
+ copilotKitCore = new CopilotKitCore({});
16
+ vi.clearAllMocks();
17
+ });
18
+
19
+ afterEach(() => {
20
+ vi.restoreAllMocks();
21
+ });
22
+
23
+ it("should skip tool call when result already exists in newMessages", async () => {
24
+ const tool = createTool({
25
+ name: "alreadyProcessedTool",
26
+ handler: vi.fn(async () => "Should not be called"),
27
+ });
28
+ copilotKitCore.addTool(tool);
29
+
30
+ const toolCallId = "processed-call";
31
+ const assistantMsg = createToolCallMessage("alreadyProcessedTool");
32
+ if (assistantMsg.role === 'assistant' && assistantMsg.toolCalls && assistantMsg.toolCalls[0]) {
33
+ assistantMsg.toolCalls[0].id = toolCallId;
34
+ }
35
+ const existingResult = createToolResultMessage(toolCallId, "Already processed");
36
+
37
+ const agent = new MockAgent({
38
+ newMessages: [assistantMsg, existingResult],
39
+ });
40
+
41
+ await copilotKitCore.runAgent({ agent: agent as any });
42
+
43
+ expect(tool.handler).not.toHaveBeenCalled();
44
+ });
45
+
46
+ it("should handle empty tool function name", async () => {
47
+ const message = createAssistantMessage({
48
+ content: "",
49
+ toolCalls: [{
50
+ id: "empty-name-call",
51
+ type: "function",
52
+ function: {
53
+ name: "",
54
+ arguments: "{}",
55
+ },
56
+ }],
57
+ });
58
+
59
+ const agent = new MockAgent({ newMessages: [message] });
60
+
61
+ await copilotKitCore.runAgent({ agent: agent as any });
62
+
63
+ expect(agent.messages.filter(m => m.role === "tool")).toHaveLength(0);
64
+ });
65
+
66
+ it("should handle tool arguments as empty string", async () => {
67
+ const tool = createTool({
68
+ name: "emptyArgsTool",
69
+ handler: vi.fn(async (args) => `Received: ${JSON.stringify(args)}`),
70
+ });
71
+ copilotKitCore.addTool(tool);
72
+
73
+ const message = createAssistantMessage({
74
+ content: "",
75
+ toolCalls: [{
76
+ id: "empty-args-call",
77
+ type: "function",
78
+ function: {
79
+ name: "emptyArgsTool",
80
+ arguments: "",
81
+ },
82
+ }],
83
+ });
84
+
85
+ const agent = new MockAgent({ newMessages: [message] });
86
+
87
+ await expect(copilotKitCore.runAgent({ agent: agent as any })).rejects.toThrow();
88
+ });
89
+
90
+ it("should handle very large tool result", async () => {
91
+ const largeResult = "x".repeat(100000); // 100KB string
92
+ const tool = createTool({
93
+ name: "largeTool",
94
+ handler: vi.fn(async () => largeResult),
95
+ followUp: false,
96
+ });
97
+ copilotKitCore.addTool(tool);
98
+
99
+ const message = createToolCallMessage("largeTool");
100
+ const agent = new MockAgent({ newMessages: [message] });
101
+
102
+ await copilotKitCore.runAgent({ agent: agent as any });
103
+
104
+ const toolMessage = agent.messages.find(m => m.role === "tool");
105
+ expect(toolMessage?.content).toBe(largeResult);
106
+ });
107
+
108
+ it("should handle tool handler modifying agent state", async () => {
109
+ const tool = createTool({
110
+ name: "stateTool",
111
+ handler: vi.fn(async () => {
112
+ // Try to modify agent messages during execution
113
+ agent.messages.push(createAssistantMessage({ content: "Injected" }));
114
+ return "Result";
115
+ }),
116
+ followUp: false,
117
+ });
118
+ copilotKitCore.addTool(tool);
119
+
120
+ const message = createToolCallMessage("stateTool");
121
+ const agent = new MockAgent({ newMessages: [message] });
122
+
123
+ await copilotKitCore.runAgent({ agent: agent as any });
124
+
125
+ // The injected message should be present
126
+ expect(agent.messages.some(m => m.content === "Injected")).toBe(true);
127
+ // Tool result should still be added correctly
128
+ expect(agent.messages.some(m => m.role === "tool" && m.content === "Result")).toBe(true);
129
+ });
130
+
131
+ it("should propagate errors from agent.runAgent", async () => {
132
+ const errorMessage = "Agent execution failed";
133
+ const agent = new MockAgent({
134
+ error: new Error(errorMessage)
135
+ });
136
+
137
+ await expect(copilotKitCore.runAgent({ agent: agent as any }))
138
+ .rejects
139
+ .toThrow(errorMessage);
140
+ });
141
+
142
+ it("should handle tool with invalid JSON arguments", async () => {
143
+ const toolName = "invalidJsonTool";
144
+ const tool = createTool({
145
+ name: toolName,
146
+ handler: vi.fn(async () => "Should not be called"),
147
+ });
148
+ copilotKitCore.addTool(tool);
149
+
150
+ const message = createAssistantMessage({
151
+ content: "",
152
+ toolCalls: [{
153
+ id: "tool-call-1",
154
+ type: "function",
155
+ function: {
156
+ name: toolName,
157
+ arguments: "{ invalid json",
158
+ },
159
+ }],
160
+ });
161
+ const agent = new MockAgent({ newMessages: [message] });
162
+
163
+ await expect(copilotKitCore.runAgent({ agent: agent as any })).rejects.toThrow();
164
+ expect(tool.handler).not.toHaveBeenCalled();
165
+ });
166
+ });
@@ -0,0 +1,216 @@
1
+ import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
2
+ import { CopilotKitCore } from "../core";
3
+ import { FrontendTool } from "../types";
4
+ import {
5
+ MockAgent,
6
+ createAssistantMessage,
7
+ createToolCallMessage,
8
+ createMultipleToolCallsMessage,
9
+ createTool,
10
+ } from "./test-utils";
11
+
12
+ describe("CopilotKitCore.runAgent - Follow-up Logic", () => {
13
+ let copilotKitCore: CopilotKitCore;
14
+
15
+ beforeEach(() => {
16
+ copilotKitCore = new CopilotKitCore({});
17
+ vi.clearAllMocks();
18
+ });
19
+
20
+ afterEach(() => {
21
+ vi.restoreAllMocks();
22
+ });
23
+
24
+ it("should trigger recursive call when tool.followUp is true", async () => {
25
+ const tool = createTool({
26
+ name: "followUpTool",
27
+ handler: vi.fn(async () => "Result"),
28
+ followUp: true,
29
+ });
30
+ copilotKitCore.addTool(tool);
31
+
32
+ const message = createToolCallMessage("followUpTool");
33
+ const followUpMessage = createAssistantMessage({ content: "Follow-up response" });
34
+
35
+ const agent = new MockAgent({ newMessages: [message] });
36
+ let callCount = 0;
37
+ agent.runAgentCallback = () => {
38
+ callCount++;
39
+ if (callCount === 2) {
40
+ agent.setNewMessages([followUpMessage]);
41
+ }
42
+ };
43
+
44
+ const result = await copilotKitCore.runAgent({ agent: agent as any });
45
+
46
+ expect(agent.runAgentCalls).toHaveLength(2);
47
+ expect(result.newMessages).toContain(followUpMessage);
48
+ });
49
+
50
+ it("should not trigger recursive call when tool.followUp is false", async () => {
51
+ const tool = createTool({
52
+ name: "noFollowUpTool",
53
+ handler: vi.fn(async () => "Result"),
54
+ followUp: false,
55
+ });
56
+ copilotKitCore.addTool(tool);
57
+
58
+ const message = createToolCallMessage("noFollowUpTool");
59
+ const agent = new MockAgent({ newMessages: [message] });
60
+
61
+ await copilotKitCore.runAgent({ agent: agent as any });
62
+
63
+ expect(agent.runAgentCalls).toHaveLength(1);
64
+ });
65
+
66
+ it("should trigger recursive call when tool.followUp is undefined (default)", async () => {
67
+ const tool: FrontendTool = {
68
+ name: "defaultFollowUpTool",
69
+ handler: vi.fn(async () => "Result"),
70
+ // followUp is undefined
71
+ };
72
+ copilotKitCore.addTool(tool);
73
+
74
+ const message = createToolCallMessage("defaultFollowUpTool");
75
+ const followUpMessage = createAssistantMessage({ content: "Follow-up" });
76
+
77
+ const agent = new MockAgent({ newMessages: [message] });
78
+ let callCount = 0;
79
+ agent.runAgentCallback = () => {
80
+ callCount++;
81
+ if (callCount === 2) {
82
+ agent.setNewMessages([followUpMessage]);
83
+ }
84
+ };
85
+
86
+ await copilotKitCore.runAgent({ agent: agent as any });
87
+
88
+ expect(agent.runAgentCalls).toHaveLength(2);
89
+ });
90
+
91
+ it("should trigger follow-up when at least one tool needs it", async () => {
92
+ const tool1 = createTool({
93
+ name: "tool1",
94
+ handler: vi.fn(async () => "Result 1"),
95
+ followUp: false,
96
+ });
97
+ const tool2 = createTool({
98
+ name: "tool2",
99
+ handler: vi.fn(async () => "Result 2"),
100
+ followUp: true,
101
+ });
102
+ const tool3 = createTool({
103
+ name: "tool3",
104
+ handler: vi.fn(async () => "Result 3"),
105
+ followUp: false,
106
+ });
107
+ copilotKitCore.addTool(tool1);
108
+ copilotKitCore.addTool(tool2);
109
+ copilotKitCore.addTool(tool3);
110
+
111
+ const message = createMultipleToolCallsMessage([
112
+ { name: "tool1" },
113
+ { name: "tool2" },
114
+ { name: "tool3" },
115
+ ]);
116
+
117
+ const agent = new MockAgent({ newMessages: [message] });
118
+ let callCount = 0;
119
+ agent.runAgentCallback = () => {
120
+ callCount++;
121
+ if (callCount === 2) {
122
+ agent.setNewMessages([]);
123
+ }
124
+ };
125
+
126
+ await copilotKitCore.runAgent({ agent: agent as any });
127
+
128
+ expect(agent.runAgentCalls).toHaveLength(2);
129
+ });
130
+
131
+ it("should not trigger follow-up when all tools have followUp=false", async () => {
132
+ const tool1 = createTool({
133
+ name: "tool1",
134
+ handler: vi.fn(async () => "Result 1"),
135
+ followUp: false,
136
+ });
137
+ const tool2 = createTool({
138
+ name: "tool2",
139
+ handler: vi.fn(async () => "Result 2"),
140
+ followUp: false,
141
+ });
142
+ copilotKitCore.addTool(tool1);
143
+ copilotKitCore.addTool(tool2);
144
+
145
+ const message = createMultipleToolCallsMessage([
146
+ { name: "tool1" },
147
+ { name: "tool2" },
148
+ ]);
149
+
150
+ const agent = new MockAgent({ newMessages: [message] });
151
+
152
+ await copilotKitCore.runAgent({ agent: agent as any });
153
+
154
+ expect(agent.runAgentCalls).toHaveLength(1);
155
+ });
156
+
157
+ it("should return final result after recursive follow-up", async () => {
158
+ const tool = createTool({
159
+ name: "recursiveTool",
160
+ handler: vi.fn(async () => "Tool result"),
161
+ followUp: true,
162
+ });
163
+ copilotKitCore.addTool(tool);
164
+
165
+ const initialMessage = createToolCallMessage("recursiveTool");
166
+ const finalMessage = createAssistantMessage({ content: "Final response" });
167
+
168
+ const agent = new MockAgent({ newMessages: [initialMessage] });
169
+ let callCount = 0;
170
+ agent.runAgentCallback = () => {
171
+ callCount++;
172
+ if (callCount === 2) {
173
+ agent.setNewMessages([finalMessage]);
174
+ }
175
+ };
176
+
177
+ const result = await copilotKitCore.runAgent({ agent: agent as any });
178
+
179
+ expect(result.newMessages).toEqual([finalMessage]);
180
+ });
181
+
182
+ it("should handle multiple recursive follow-ups (chain)", async () => {
183
+ const tool1 = createTool({
184
+ name: "chainTool1",
185
+ handler: vi.fn(async () => "Result 1"),
186
+ followUp: true,
187
+ });
188
+ const tool2 = createTool({
189
+ name: "chainTool2",
190
+ handler: vi.fn(async () => "Result 2"),
191
+ followUp: true,
192
+ });
193
+ copilotKitCore.addTool(tool1);
194
+ copilotKitCore.addTool(tool2);
195
+
196
+ const msg1 = createToolCallMessage("chainTool1");
197
+ const msg2 = createToolCallMessage("chainTool2");
198
+ const finalMsg = createAssistantMessage({ content: "Done" });
199
+
200
+ const agent = new MockAgent({ newMessages: [msg1] });
201
+ let callCount = 0;
202
+ agent.runAgentCallback = () => {
203
+ callCount++;
204
+ if (callCount === 2) {
205
+ agent.setNewMessages([msg2]);
206
+ } else if (callCount === 3) {
207
+ agent.setNewMessages([finalMsg]);
208
+ }
209
+ };
210
+
211
+ const result = await copilotKitCore.runAgent({ agent: agent as any });
212
+
213
+ expect(agent.runAgentCalls).toHaveLength(3);
214
+ expect(result.newMessages).toEqual([finalMsg]);
215
+ });
216
+ });