@copilotkit/runtime 1.55.1 → 1.55.2-next.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/CHANGELOG.md +13 -0
- package/dist/agent/converters/aisdk.cjs +215 -0
- package/dist/agent/converters/aisdk.cjs.map +1 -0
- package/dist/agent/converters/aisdk.d.cts +18 -0
- package/dist/agent/converters/aisdk.d.cts.map +1 -0
- package/dist/agent/converters/aisdk.d.mts +18 -0
- package/dist/agent/converters/aisdk.d.mts.map +1 -0
- package/dist/agent/converters/aisdk.mjs +214 -0
- package/dist/agent/converters/aisdk.mjs.map +1 -0
- package/dist/agent/converters/index.d.mts +3 -0
- package/dist/agent/converters/tanstack.cjs +180 -0
- package/dist/agent/converters/tanstack.cjs.map +1 -0
- package/dist/agent/converters/tanstack.d.cts +68 -0
- package/dist/agent/converters/tanstack.d.cts.map +1 -0
- package/dist/agent/converters/tanstack.d.mts +68 -0
- package/dist/agent/converters/tanstack.d.mts.map +1 -0
- package/dist/agent/converters/tanstack.mjs +178 -0
- package/dist/agent/converters/tanstack.mjs.map +1 -0
- package/dist/agent/index.cjs +111 -17
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts +61 -4
- package/dist/agent/index.d.cts.map +1 -1
- package/dist/agent/index.d.mts +62 -4
- package/dist/agent/index.d.mts.map +1 -1
- package/dist/agent/index.mjs +111 -17
- package/dist/agent/index.mjs.map +1 -1
- package/dist/lib/integrations/nextjs/pages-router.cjs.map +1 -1
- package/dist/lib/integrations/nextjs/pages-router.d.cts.map +1 -1
- package/dist/lib/integrations/nextjs/pages-router.d.mts.map +1 -1
- package/dist/lib/integrations/nextjs/pages-router.mjs.map +1 -1
- package/dist/lib/runtime/copilot-runtime.cjs +4 -2
- package/dist/lib/runtime/copilot-runtime.cjs.map +1 -1
- package/dist/lib/runtime/copilot-runtime.d.cts.map +1 -1
- package/dist/lib/runtime/copilot-runtime.d.mts.map +1 -1
- package/dist/lib/runtime/copilot-runtime.mjs +4 -2
- package/dist/lib/runtime/copilot-runtime.mjs.map +1 -1
- package/dist/lib/runtime/mcp-tools-utils.cjs +1 -1
- package/dist/lib/runtime/mcp-tools-utils.cjs.map +1 -1
- package/dist/lib/runtime/mcp-tools-utils.mjs +1 -1
- package/dist/lib/runtime/mcp-tools-utils.mjs.map +1 -1
- package/dist/package.cjs +3 -2
- package/dist/package.mjs +3 -2
- package/dist/service-adapters/anthropic/utils.cjs +1 -1
- package/dist/service-adapters/anthropic/utils.cjs.map +1 -1
- package/dist/service-adapters/anthropic/utils.mjs +1 -1
- package/dist/service-adapters/anthropic/utils.mjs.map +1 -1
- package/dist/service-adapters/openai/utils.cjs +1 -1
- package/dist/service-adapters/openai/utils.cjs.map +1 -1
- package/dist/service-adapters/openai/utils.mjs +1 -1
- package/dist/service-adapters/openai/utils.mjs.map +1 -1
- package/dist/v2/index.cjs +5 -0
- package/dist/v2/index.d.cts +4 -2
- package/dist/v2/index.d.mts +4 -2
- package/dist/v2/index.mjs +3 -1
- package/package.json +4 -3
- package/src/agent/__tests__/agent-test-helpers.ts +446 -0
- package/src/agent/__tests__/agent.test.ts +593 -0
- package/src/agent/__tests__/converter-aisdk.test.ts +692 -0
- package/src/agent/__tests__/converter-custom.test.ts +319 -0
- package/src/agent/__tests__/converter-tanstack-input.test.ts +211 -0
- package/src/agent/__tests__/converter-tanstack.test.ts +314 -0
- package/src/agent/__tests__/mcp-servers-integration.test.ts +373 -0
- package/src/agent/__tests__/multimodal-tanstack.test.ts +284 -0
- package/src/agent/__tests__/test-helpers.ts +12 -8
- package/src/agent/converters/aisdk.ts +326 -0
- package/src/agent/converters/index.ts +7 -0
- package/src/agent/converters/tanstack.ts +286 -0
- package/src/agent/index.ts +245 -26
- package/src/lib/integrations/nextjs/pages-router.ts +1 -0
- package/src/lib/runtime/copilot-runtime.ts +21 -12
- package/src/lib/runtime/mcp-tools-utils.ts +1 -1
- package/src/service-adapters/anthropic/utils.ts +1 -1
- package/src/service-adapters/openai/utils.ts +1 -1
- package/src/v2/runtime/__tests__/mcp-apps-middleware-integration.test.ts +275 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { EventType, type BaseEvent } from "@ag-ui/client";
|
|
3
|
+
import { BuiltInAgent } from "../index";
|
|
4
|
+
import {
|
|
5
|
+
createAgent,
|
|
6
|
+
createDefaultInput,
|
|
7
|
+
collectEvents,
|
|
8
|
+
expectLifecycleWrapped,
|
|
9
|
+
expectEventSequence,
|
|
10
|
+
eventField,
|
|
11
|
+
mockCustomStream,
|
|
12
|
+
} from "./agent-test-helpers";
|
|
13
|
+
|
|
14
|
+
describe("Custom Converter (passthrough)", () => {
|
|
15
|
+
// -----------------------------------------------------------------------
|
|
16
|
+
// Event Forwarding
|
|
17
|
+
// -----------------------------------------------------------------------
|
|
18
|
+
describe("Event Forwarding", () => {
|
|
19
|
+
it("should forward a single TEXT_MESSAGE_CHUNK as-is between lifecycle events", async () => {
|
|
20
|
+
const chunk: BaseEvent = {
|
|
21
|
+
type: EventType.TEXT_MESSAGE_CHUNK,
|
|
22
|
+
role: "assistant",
|
|
23
|
+
delta: "Hello world",
|
|
24
|
+
} as BaseEvent;
|
|
25
|
+
|
|
26
|
+
const agent = createAgent("custom", [chunk]);
|
|
27
|
+
const input = createDefaultInput();
|
|
28
|
+
const events = await collectEvents(agent.run(input));
|
|
29
|
+
|
|
30
|
+
expectLifecycleWrapped(events, "test-thread", "test-run");
|
|
31
|
+
expectEventSequence(events, [
|
|
32
|
+
EventType.RUN_STARTED,
|
|
33
|
+
EventType.TEXT_MESSAGE_CHUNK,
|
|
34
|
+
EventType.RUN_FINISHED,
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
expect(eventField<string>(events[1], "delta")).toBe("Hello world");
|
|
38
|
+
expect(eventField<string>(events[1], "role")).toBe("assistant");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should forward multiple event types in order", async () => {
|
|
42
|
+
const userEvents: BaseEvent[] = [
|
|
43
|
+
{ type: EventType.TEXT_MESSAGE_START, role: "assistant" } as BaseEvent,
|
|
44
|
+
{
|
|
45
|
+
type: EventType.TEXT_MESSAGE_CONTENT,
|
|
46
|
+
role: "assistant",
|
|
47
|
+
content: "Hi",
|
|
48
|
+
} as BaseEvent,
|
|
49
|
+
{ type: EventType.TEXT_MESSAGE_END } as BaseEvent,
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const agent = createAgent("custom", userEvents);
|
|
53
|
+
const events = await collectEvents(agent.run(createDefaultInput()));
|
|
54
|
+
|
|
55
|
+
expectEventSequence(events, [
|
|
56
|
+
EventType.RUN_STARTED,
|
|
57
|
+
EventType.TEXT_MESSAGE_START,
|
|
58
|
+
EventType.TEXT_MESSAGE_CONTENT,
|
|
59
|
+
EventType.TEXT_MESSAGE_END,
|
|
60
|
+
EventType.RUN_FINISHED,
|
|
61
|
+
]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("should forward TOOL_CALL_START + TOOL_CALL_ARGS + TOOL_CALL_END", async () => {
|
|
65
|
+
const toolEvents: BaseEvent[] = [
|
|
66
|
+
{
|
|
67
|
+
type: EventType.TOOL_CALL_START,
|
|
68
|
+
toolCallId: "tc-1",
|
|
69
|
+
toolCallName: "myTool",
|
|
70
|
+
} as BaseEvent,
|
|
71
|
+
{
|
|
72
|
+
type: EventType.TOOL_CALL_ARGS,
|
|
73
|
+
toolCallId: "tc-1",
|
|
74
|
+
delta: '{"key":"value"}',
|
|
75
|
+
} as BaseEvent,
|
|
76
|
+
{
|
|
77
|
+
type: EventType.TOOL_CALL_END,
|
|
78
|
+
toolCallId: "tc-1",
|
|
79
|
+
} as BaseEvent,
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
const agent = createAgent("custom", toolEvents);
|
|
83
|
+
const events = await collectEvents(agent.run(createDefaultInput()));
|
|
84
|
+
|
|
85
|
+
expectEventSequence(events, [
|
|
86
|
+
EventType.RUN_STARTED,
|
|
87
|
+
EventType.TOOL_CALL_START,
|
|
88
|
+
EventType.TOOL_CALL_ARGS,
|
|
89
|
+
EventType.TOOL_CALL_END,
|
|
90
|
+
EventType.RUN_FINISHED,
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
expect(eventField<string>(events[1], "toolCallId")).toBe("tc-1");
|
|
94
|
+
expect(eventField<string>(events[1], "toolCallName")).toBe("myTool");
|
|
95
|
+
|
|
96
|
+
expect(eventField<string>(events[2], "toolCallId")).toBe("tc-1");
|
|
97
|
+
expect(eventField<string>(events[2], "delta")).toBe('{"key":"value"}');
|
|
98
|
+
|
|
99
|
+
expect(eventField<string>(events[3], "toolCallId")).toBe("tc-1");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("should forward a STATE_SNAPSHOT event", async () => {
|
|
103
|
+
const snapshot: BaseEvent = {
|
|
104
|
+
type: EventType.STATE_SNAPSHOT,
|
|
105
|
+
snapshot: { counter: 42, items: ["a", "b"] },
|
|
106
|
+
} as BaseEvent;
|
|
107
|
+
|
|
108
|
+
const agent = createAgent("custom", [snapshot]);
|
|
109
|
+
const events = await collectEvents(agent.run(createDefaultInput()));
|
|
110
|
+
|
|
111
|
+
expectEventSequence(events, [
|
|
112
|
+
EventType.RUN_STARTED,
|
|
113
|
+
EventType.STATE_SNAPSHOT,
|
|
114
|
+
EventType.RUN_FINISHED,
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
expect(
|
|
118
|
+
eventField<Record<string, unknown>>(events[1], "snapshot"),
|
|
119
|
+
).toEqual({ counter: 42, items: ["a", "b"] });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should forward a STATE_DELTA event", async () => {
|
|
123
|
+
const delta: BaseEvent = {
|
|
124
|
+
type: EventType.STATE_DELTA,
|
|
125
|
+
delta: [{ op: "replace", path: "/counter", value: 43 }],
|
|
126
|
+
} as BaseEvent;
|
|
127
|
+
|
|
128
|
+
const agent = createAgent("custom", [delta]);
|
|
129
|
+
const events = await collectEvents(agent.run(createDefaultInput()));
|
|
130
|
+
|
|
131
|
+
expectEventSequence(events, [
|
|
132
|
+
EventType.RUN_STARTED,
|
|
133
|
+
EventType.STATE_DELTA,
|
|
134
|
+
EventType.RUN_FINISHED,
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
expect(eventField<unknown[]>(events[1], "delta")).toEqual([
|
|
138
|
+
{ op: "replace", path: "/counter", value: 43 },
|
|
139
|
+
]);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("should forward reasoning events in order", async () => {
|
|
143
|
+
const reasoningEvents: BaseEvent[] = [
|
|
144
|
+
{ type: EventType.REASONING_START } as BaseEvent,
|
|
145
|
+
{ type: EventType.REASONING_MESSAGE_START } as BaseEvent,
|
|
146
|
+
{
|
|
147
|
+
type: EventType.REASONING_MESSAGE_CONTENT,
|
|
148
|
+
content: "Thinking step 1",
|
|
149
|
+
} as BaseEvent,
|
|
150
|
+
{
|
|
151
|
+
type: EventType.REASONING_MESSAGE_CONTENT,
|
|
152
|
+
content: "Thinking step 2",
|
|
153
|
+
} as BaseEvent,
|
|
154
|
+
{ type: EventType.REASONING_MESSAGE_END } as BaseEvent,
|
|
155
|
+
{ type: EventType.REASONING_END } as BaseEvent,
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
const agent = createAgent("custom", reasoningEvents);
|
|
159
|
+
const events = await collectEvents(agent.run(createDefaultInput()));
|
|
160
|
+
|
|
161
|
+
expectEventSequence(events, [
|
|
162
|
+
EventType.RUN_STARTED,
|
|
163
|
+
EventType.REASONING_START,
|
|
164
|
+
EventType.REASONING_MESSAGE_START,
|
|
165
|
+
EventType.REASONING_MESSAGE_CONTENT,
|
|
166
|
+
EventType.REASONING_MESSAGE_CONTENT,
|
|
167
|
+
EventType.REASONING_MESSAGE_END,
|
|
168
|
+
EventType.REASONING_END,
|
|
169
|
+
EventType.RUN_FINISHED,
|
|
170
|
+
]);
|
|
171
|
+
|
|
172
|
+
expect(eventField<string>(events[3], "content")).toBe("Thinking step 1");
|
|
173
|
+
expect(eventField<string>(events[4], "content")).toBe("Thinking step 2");
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// -----------------------------------------------------------------------
|
|
178
|
+
// Lifecycle Boundary
|
|
179
|
+
// -----------------------------------------------------------------------
|
|
180
|
+
describe("Lifecycle Boundary", () => {
|
|
181
|
+
it("should result in duplicate RUN_STARTED when user emits one in custom stream", async () => {
|
|
182
|
+
const userEvents: BaseEvent[] = [
|
|
183
|
+
{
|
|
184
|
+
type: EventType.RUN_STARTED,
|
|
185
|
+
threadId: "user-thread",
|
|
186
|
+
runId: "user-run",
|
|
187
|
+
} as BaseEvent,
|
|
188
|
+
{
|
|
189
|
+
type: EventType.TEXT_MESSAGE_CHUNK,
|
|
190
|
+
role: "assistant",
|
|
191
|
+
delta: "Hello",
|
|
192
|
+
} as BaseEvent,
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
const agent = createAgent("custom", userEvents);
|
|
196
|
+
const events = await collectEvents(agent.run(createDefaultInput()));
|
|
197
|
+
|
|
198
|
+
// Agent emits its own RUN_STARTED, then the user's RUN_STARTED is forwarded
|
|
199
|
+
const runStartedEvents = events.filter(
|
|
200
|
+
(e) => e.type === EventType.RUN_STARTED,
|
|
201
|
+
);
|
|
202
|
+
expect(runStartedEvents).toHaveLength(2);
|
|
203
|
+
|
|
204
|
+
// First is from the Agent lifecycle
|
|
205
|
+
expect(runStartedEvents[0]).toMatchObject({
|
|
206
|
+
type: EventType.RUN_STARTED,
|
|
207
|
+
threadId: "test-thread",
|
|
208
|
+
runId: "test-run",
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Second is the user-emitted one, forwarded as-is
|
|
212
|
+
expect(eventField<string>(runStartedEvents[1], "threadId")).toBe(
|
|
213
|
+
"user-thread",
|
|
214
|
+
);
|
|
215
|
+
expect(eventField<string>(runStartedEvents[1], "runId")).toBe("user-run");
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// -----------------------------------------------------------------------
|
|
220
|
+
// Edge Cases
|
|
221
|
+
// -----------------------------------------------------------------------
|
|
222
|
+
describe("Edge Cases", () => {
|
|
223
|
+
it("should emit only lifecycle events for an empty async iterable", async () => {
|
|
224
|
+
const agent = createAgent("custom", []);
|
|
225
|
+
const events = await collectEvents(agent.run(createDefaultInput()));
|
|
226
|
+
|
|
227
|
+
expectEventSequence(events, [
|
|
228
|
+
EventType.RUN_STARTED,
|
|
229
|
+
EventType.RUN_FINISHED,
|
|
230
|
+
]);
|
|
231
|
+
expectLifecycleWrapped(events, "test-thread", "test-run");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("should work correctly with an async generator factory", async () => {
|
|
235
|
+
const agent = new BuiltInAgent({
|
|
236
|
+
type: "custom",
|
|
237
|
+
factory: async function* () {
|
|
238
|
+
yield {
|
|
239
|
+
type: EventType.TEXT_MESSAGE_CHUNK,
|
|
240
|
+
role: "assistant",
|
|
241
|
+
delta: "from generator",
|
|
242
|
+
} as BaseEvent;
|
|
243
|
+
yield {
|
|
244
|
+
type: EventType.TEXT_MESSAGE_CHUNK,
|
|
245
|
+
role: "assistant",
|
|
246
|
+
delta: " factory",
|
|
247
|
+
} as BaseEvent;
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const events = await collectEvents(agent.run(createDefaultInput()));
|
|
252
|
+
|
|
253
|
+
expectEventSequence(events, [
|
|
254
|
+
EventType.RUN_STARTED,
|
|
255
|
+
EventType.TEXT_MESSAGE_CHUNK,
|
|
256
|
+
EventType.TEXT_MESSAGE_CHUNK,
|
|
257
|
+
EventType.RUN_FINISHED,
|
|
258
|
+
]);
|
|
259
|
+
|
|
260
|
+
expect(eventField<string>(events[1], "delta")).toBe("from generator");
|
|
261
|
+
expect(eventField<string>(events[2], "delta")).toBe(" factory");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("should pass through events with extra/unknown fields", async () => {
|
|
265
|
+
const eventWithExtras: BaseEvent = {
|
|
266
|
+
type: EventType.CUSTOM,
|
|
267
|
+
customField: "custom-value",
|
|
268
|
+
nestedData: { deep: { value: 123 } },
|
|
269
|
+
arrayField: [1, 2, 3],
|
|
270
|
+
} as BaseEvent;
|
|
271
|
+
|
|
272
|
+
const agent = createAgent("custom", [eventWithExtras]);
|
|
273
|
+
const events = await collectEvents(agent.run(createDefaultInput()));
|
|
274
|
+
|
|
275
|
+
expectEventSequence(events, [
|
|
276
|
+
EventType.RUN_STARTED,
|
|
277
|
+
EventType.CUSTOM,
|
|
278
|
+
EventType.RUN_FINISHED,
|
|
279
|
+
]);
|
|
280
|
+
|
|
281
|
+
expect(eventField<string>(events[1], "customField")).toBe("custom-value");
|
|
282
|
+
expect(
|
|
283
|
+
eventField<{ deep: { value: number } }>(events[1], "nestedData"),
|
|
284
|
+
).toEqual({ deep: { value: 123 } });
|
|
285
|
+
expect(eventField<number[]>(events[1], "arrayField")).toEqual([1, 2, 3]);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("should forward 1000+ events without loss", async () => {
|
|
289
|
+
const count = 1500;
|
|
290
|
+
const manyEvents: BaseEvent[] = Array.from({ length: count }, (_, i) => ({
|
|
291
|
+
type: EventType.TEXT_MESSAGE_CHUNK,
|
|
292
|
+
role: "assistant",
|
|
293
|
+
delta: `chunk-${i}`,
|
|
294
|
+
})) as BaseEvent[];
|
|
295
|
+
|
|
296
|
+
const agent = createAgent("custom", manyEvents);
|
|
297
|
+
const events = await collectEvents(agent.run(createDefaultInput()));
|
|
298
|
+
|
|
299
|
+
// Total = RUN_STARTED + 1500 chunks + RUN_FINISHED
|
|
300
|
+
expect(events).toHaveLength(count + 2);
|
|
301
|
+
|
|
302
|
+
// First and last are lifecycle
|
|
303
|
+
expect(events[0].type).toBe(EventType.RUN_STARTED);
|
|
304
|
+
expect(events[events.length - 1].type).toBe(EventType.RUN_FINISHED);
|
|
305
|
+
|
|
306
|
+
// All content events are TEXT_MESSAGE_CHUNK
|
|
307
|
+
const contentEvents = events.slice(1, -1);
|
|
308
|
+
expect(contentEvents).toHaveLength(count);
|
|
309
|
+
|
|
310
|
+
// Verify order preservation
|
|
311
|
+
for (let i = 0; i < count; i++) {
|
|
312
|
+
expect(contentEvents[i].type).toBe(EventType.TEXT_MESSAGE_CHUNK);
|
|
313
|
+
expect(eventField<string>(contentEvents[i], "delta")).toBe(
|
|
314
|
+
`chunk-${i}`,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
});
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { convertInputToTanStackAI } from "../converters/tanstack";
|
|
3
|
+
import { createDefaultInput } from "./agent-test-helpers";
|
|
4
|
+
|
|
5
|
+
describe("convertInputToTanStackAI", () => {
|
|
6
|
+
// -------------------------------------------------------------------------
|
|
7
|
+
// Message filtering
|
|
8
|
+
// -------------------------------------------------------------------------
|
|
9
|
+
describe("message filtering", () => {
|
|
10
|
+
it("filters out system messages from the messages array", () => {
|
|
11
|
+
const input = createDefaultInput({
|
|
12
|
+
messages: [
|
|
13
|
+
{ role: "system", content: "You are helpful" },
|
|
14
|
+
{ role: "user", content: "Hello" },
|
|
15
|
+
],
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const { messages } = convertInputToTanStackAI(input);
|
|
19
|
+
|
|
20
|
+
expect(messages).toHaveLength(1);
|
|
21
|
+
expect(messages[0].role).toBe("user");
|
|
22
|
+
expect(messages[0].content).toBe("Hello");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("filters out developer messages from the messages array", () => {
|
|
26
|
+
const input = createDefaultInput({
|
|
27
|
+
messages: [
|
|
28
|
+
{ role: "developer", content: "Internal instruction" },
|
|
29
|
+
{ role: "user", content: "Hi" },
|
|
30
|
+
],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const { messages } = convertInputToTanStackAI(input);
|
|
34
|
+
|
|
35
|
+
expect(messages).toHaveLength(1);
|
|
36
|
+
expect(messages[0].role).toBe("user");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("extracts system and developer messages into systemPrompts", () => {
|
|
40
|
+
const input = createDefaultInput({
|
|
41
|
+
messages: [
|
|
42
|
+
{ role: "system", content: "System prompt" },
|
|
43
|
+
{ role: "developer", content: "Dev instruction" },
|
|
44
|
+
{ role: "user", content: "Hello" },
|
|
45
|
+
],
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const { systemPrompts } = convertInputToTanStackAI(input);
|
|
49
|
+
|
|
50
|
+
expect(systemPrompts).toContain("System prompt");
|
|
51
|
+
expect(systemPrompts).toContain("Dev instruction");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("preserves user and assistant messages in order", () => {
|
|
55
|
+
const input = createDefaultInput({
|
|
56
|
+
messages: [
|
|
57
|
+
{ role: "user", content: "Question 1" },
|
|
58
|
+
{ role: "assistant", content: "Answer 1" },
|
|
59
|
+
{ role: "user", content: "Question 2" },
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const { messages } = convertInputToTanStackAI(input);
|
|
64
|
+
|
|
65
|
+
expect(messages).toHaveLength(3);
|
|
66
|
+
expect(messages[0]).toMatchObject({
|
|
67
|
+
role: "user",
|
|
68
|
+
content: "Question 1",
|
|
69
|
+
});
|
|
70
|
+
expect(messages[1]).toMatchObject({
|
|
71
|
+
role: "assistant",
|
|
72
|
+
content: "Answer 1",
|
|
73
|
+
});
|
|
74
|
+
expect(messages[2]).toMatchObject({
|
|
75
|
+
role: "user",
|
|
76
|
+
content: "Question 2",
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// -------------------------------------------------------------------------
|
|
82
|
+
// Tool call mapping
|
|
83
|
+
// -------------------------------------------------------------------------
|
|
84
|
+
describe("tool call mapping", () => {
|
|
85
|
+
it("maps assistant message toolCalls to TanStack format", () => {
|
|
86
|
+
const input = createDefaultInput({
|
|
87
|
+
messages: [
|
|
88
|
+
{
|
|
89
|
+
role: "assistant",
|
|
90
|
+
content: null,
|
|
91
|
+
toolCalls: [
|
|
92
|
+
{
|
|
93
|
+
id: "tc-1",
|
|
94
|
+
type: "function",
|
|
95
|
+
function: { name: "getWeather", arguments: '{"city":"NYC"}' },
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const { messages } = convertInputToTanStackAI(input);
|
|
103
|
+
|
|
104
|
+
expect(messages).toHaveLength(1);
|
|
105
|
+
expect(messages[0].toolCalls).toHaveLength(1);
|
|
106
|
+
expect(messages[0].toolCalls![0]).toEqual({
|
|
107
|
+
id: "tc-1",
|
|
108
|
+
type: "function",
|
|
109
|
+
function: { name: "getWeather", arguments: '{"city":"NYC"}' },
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("maps tool messages with toolCallId", () => {
|
|
114
|
+
const input = createDefaultInput({
|
|
115
|
+
messages: [
|
|
116
|
+
{
|
|
117
|
+
role: "tool",
|
|
118
|
+
content: '{"temp": 72}',
|
|
119
|
+
toolCallId: "tc-1",
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const { messages } = convertInputToTanStackAI(input);
|
|
125
|
+
|
|
126
|
+
expect(messages).toHaveLength(1);
|
|
127
|
+
expect(messages[0].role).toBe("tool");
|
|
128
|
+
expect(messages[0].toolCallId).toBe("tc-1");
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// -------------------------------------------------------------------------
|
|
133
|
+
// Context injection
|
|
134
|
+
// -------------------------------------------------------------------------
|
|
135
|
+
describe("context injection", () => {
|
|
136
|
+
it("appends context entries to systemPrompts", () => {
|
|
137
|
+
const input = createDefaultInput({
|
|
138
|
+
context: [
|
|
139
|
+
{ description: "User preferences", value: "Dark mode enabled" },
|
|
140
|
+
{ description: "Current page", value: "/dashboard" },
|
|
141
|
+
],
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const { systemPrompts } = convertInputToTanStackAI(input);
|
|
145
|
+
|
|
146
|
+
expect(systemPrompts).toContain("User preferences:\nDark mode enabled");
|
|
147
|
+
expect(systemPrompts).toContain("Current page:\n/dashboard");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("does not add context when context array is empty", () => {
|
|
151
|
+
const input = createDefaultInput({ context: [] });
|
|
152
|
+
|
|
153
|
+
const { systemPrompts } = convertInputToTanStackAI(input);
|
|
154
|
+
|
|
155
|
+
expect(systemPrompts).toHaveLength(0);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// -------------------------------------------------------------------------
|
|
160
|
+
// State serialization
|
|
161
|
+
// -------------------------------------------------------------------------
|
|
162
|
+
describe("state serialization", () => {
|
|
163
|
+
it("serializes non-empty state into systemPrompts", () => {
|
|
164
|
+
const input = createDefaultInput({
|
|
165
|
+
state: { count: 42, items: ["a", "b"] },
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const { systemPrompts } = convertInputToTanStackAI(input);
|
|
169
|
+
|
|
170
|
+
const statePrompt = systemPrompts.find((p) =>
|
|
171
|
+
p.startsWith("Application State:"),
|
|
172
|
+
);
|
|
173
|
+
expect(statePrompt).toBeDefined();
|
|
174
|
+
expect(statePrompt).toContain('"count": 42');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("does not add state when state is empty object", () => {
|
|
178
|
+
const input = createDefaultInput({ state: {} });
|
|
179
|
+
|
|
180
|
+
const { systemPrompts } = convertInputToTanStackAI(input);
|
|
181
|
+
|
|
182
|
+
expect(
|
|
183
|
+
systemPrompts.find((p) => p.startsWith("Application State:")),
|
|
184
|
+
).toBeUndefined();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("does not add state when state is null", () => {
|
|
188
|
+
const input = createDefaultInput({ state: null });
|
|
189
|
+
|
|
190
|
+
const { systemPrompts } = convertInputToTanStackAI(input);
|
|
191
|
+
|
|
192
|
+
expect(
|
|
193
|
+
systemPrompts.find((p) => p.startsWith("Application State:")),
|
|
194
|
+
).toBeUndefined();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// -------------------------------------------------------------------------
|
|
199
|
+
// Empty input
|
|
200
|
+
// -------------------------------------------------------------------------
|
|
201
|
+
describe("empty input", () => {
|
|
202
|
+
it("returns empty messages and systemPrompts for default input", () => {
|
|
203
|
+
const input = createDefaultInput();
|
|
204
|
+
|
|
205
|
+
const { messages, systemPrompts } = convertInputToTanStackAI(input);
|
|
206
|
+
|
|
207
|
+
expect(messages).toHaveLength(0);
|
|
208
|
+
expect(systemPrompts).toHaveLength(0);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
});
|