@copilotkit/runtime 1.55.2-next.0 → 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 +7 -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 +1 -1
- package/dist/package.mjs +1 -1
- 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 +2 -2
- 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__/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
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { EventType, type BaseEvent } from "@ag-ui/client";
|
|
3
|
+
import {
|
|
4
|
+
BuiltInAgent,
|
|
5
|
+
type AgentFactoryContext,
|
|
6
|
+
type BuiltInAgentFactoryConfig,
|
|
7
|
+
createDefaultInput,
|
|
8
|
+
createAgent,
|
|
9
|
+
createThrowingAgent,
|
|
10
|
+
createMidStreamErrorAgent,
|
|
11
|
+
collectEvents,
|
|
12
|
+
collectEventsIncludingErrors,
|
|
13
|
+
expectLifecycleWrapped,
|
|
14
|
+
eventField,
|
|
15
|
+
textDelta,
|
|
16
|
+
finish,
|
|
17
|
+
tanstackTextChunk,
|
|
18
|
+
type AgentType,
|
|
19
|
+
type MockStreamEvent,
|
|
20
|
+
} from "./agent-test-helpers";
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Local helpers for parameterized tests
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
const allTypes: AgentType[] = ["aisdk", "tanstack", "custom"];
|
|
27
|
+
|
|
28
|
+
function minimalStreamData(
|
|
29
|
+
type: AgentType,
|
|
30
|
+
): MockStreamEvent[] | Record<string, unknown>[] | BaseEvent[] {
|
|
31
|
+
switch (type) {
|
|
32
|
+
case "aisdk":
|
|
33
|
+
return [textDelta("hi"), finish()];
|
|
34
|
+
case "tanstack":
|
|
35
|
+
return [tanstackTextChunk("hi")];
|
|
36
|
+
case "custom":
|
|
37
|
+
return [
|
|
38
|
+
{
|
|
39
|
+
type: EventType.TEXT_MESSAGE_CHUNK,
|
|
40
|
+
role: "assistant",
|
|
41
|
+
delta: "hi",
|
|
42
|
+
} as BaseEvent,
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function emptyStreamData(
|
|
48
|
+
type: AgentType,
|
|
49
|
+
): MockStreamEvent[] | Record<string, unknown>[] | BaseEvent[] {
|
|
50
|
+
switch (type) {
|
|
51
|
+
case "aisdk":
|
|
52
|
+
return [finish()];
|
|
53
|
+
case "tanstack":
|
|
54
|
+
return [];
|
|
55
|
+
case "custom":
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Parameterized test suites
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
describe.each(allTypes)("Agent [%s]", (type) => {
|
|
65
|
+
// -------------------------------------------------------------------------
|
|
66
|
+
// Lifecycle
|
|
67
|
+
// -------------------------------------------------------------------------
|
|
68
|
+
describe("lifecycle", () => {
|
|
69
|
+
it("emits RUN_STARTED as the first event with correct threadId/runId", async () => {
|
|
70
|
+
const agent = createAgent(type, minimalStreamData(type));
|
|
71
|
+
const input = createDefaultInput({ threadId: "t1", runId: "r1" });
|
|
72
|
+
const events = await collectEvents(agent.run(input));
|
|
73
|
+
|
|
74
|
+
expect(events.length).toBeGreaterThanOrEqual(2);
|
|
75
|
+
const first = events[0];
|
|
76
|
+
expect(first.type).toBe(EventType.RUN_STARTED);
|
|
77
|
+
expect(eventField<string>(first, "threadId")).toBe("t1");
|
|
78
|
+
expect(eventField<string>(first, "runId")).toBe("r1");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("emits RUN_FINISHED as the last event with correct threadId/runId", async () => {
|
|
82
|
+
const agent = createAgent(type, minimalStreamData(type));
|
|
83
|
+
const input = createDefaultInput({ threadId: "t2", runId: "r2" });
|
|
84
|
+
const events = await collectEvents(agent.run(input));
|
|
85
|
+
|
|
86
|
+
const last = events[events.length - 1];
|
|
87
|
+
expect(last.type).toBe(EventType.RUN_FINISHED);
|
|
88
|
+
expect(eventField<string>(last, "threadId")).toBe("t2");
|
|
89
|
+
expect(eventField<string>(last, "runId")).toBe("r2");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("emits RUN_FINISHED for an empty stream", async () => {
|
|
93
|
+
const agent = createAgent(type, emptyStreamData(type));
|
|
94
|
+
const input = createDefaultInput();
|
|
95
|
+
const events = await collectEvents(agent.run(input));
|
|
96
|
+
|
|
97
|
+
expect(events.length).toBeGreaterThanOrEqual(2);
|
|
98
|
+
expect(events[0].type).toBe(EventType.RUN_STARTED);
|
|
99
|
+
expect(events[events.length - 1].type).toBe(EventType.RUN_FINISHED);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("wraps content with lifecycle events", async () => {
|
|
103
|
+
const agent = createAgent(type, minimalStreamData(type));
|
|
104
|
+
const input = createDefaultInput({ threadId: "wrap-t", runId: "wrap-r" });
|
|
105
|
+
const events = await collectEvents(agent.run(input));
|
|
106
|
+
|
|
107
|
+
expectLifecycleWrapped(events, "wrap-t", "wrap-r");
|
|
108
|
+
|
|
109
|
+
// There should be content events between the lifecycle bookends
|
|
110
|
+
const contentEvents = events.slice(1, -1);
|
|
111
|
+
expect(contentEvents.length).toBeGreaterThan(0);
|
|
112
|
+
for (const e of contentEvents) {
|
|
113
|
+
expect(e.type).not.toBe(EventType.RUN_STARTED);
|
|
114
|
+
expect(e.type).not.toBe(EventType.RUN_FINISHED);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// -------------------------------------------------------------------------
|
|
120
|
+
// RUN_ERROR
|
|
121
|
+
// -------------------------------------------------------------------------
|
|
122
|
+
describe("RUN_ERROR", () => {
|
|
123
|
+
it("emits RUN_ERROR when factory throws", async () => {
|
|
124
|
+
const agent = createThrowingAgent(type, "factory-boom");
|
|
125
|
+
const input = createDefaultInput();
|
|
126
|
+
const { events, errored } = await collectEventsIncludingErrors(
|
|
127
|
+
agent.run(input),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
expect(errored).toBe(true);
|
|
131
|
+
const errorEvents = events.filter((e) => e.type === EventType.RUN_ERROR);
|
|
132
|
+
expect(errorEvents.length).toBe(1);
|
|
133
|
+
expect(eventField<string>(errorEvents[0], "message")).toBe(
|
|
134
|
+
"factory-boom",
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("emits RUN_ERROR when stream throws mid-iteration", async () => {
|
|
139
|
+
const agent = createMidStreamErrorAgent(type, "mid-stream-boom");
|
|
140
|
+
const input = createDefaultInput();
|
|
141
|
+
const { events, errored } = await collectEventsIncludingErrors(
|
|
142
|
+
agent.run(input),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
expect(errored).toBe(true);
|
|
146
|
+
const errorEvents = events.filter((e) => e.type === EventType.RUN_ERROR);
|
|
147
|
+
expect(errorEvents.length).toBe(1);
|
|
148
|
+
expect(eventField<string>(errorEvents[0], "message")).toBe(
|
|
149
|
+
"mid-stream-boom",
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("does not emit RUN_FINISHED after RUN_ERROR", async () => {
|
|
154
|
+
const agent = createThrowingAgent(type, "no-finish");
|
|
155
|
+
const input = createDefaultInput();
|
|
156
|
+
const { events } = await collectEventsIncludingErrors(agent.run(input));
|
|
157
|
+
|
|
158
|
+
const errorIdx = events.findIndex((e) => e.type === EventType.RUN_ERROR);
|
|
159
|
+
expect(errorIdx).toBeGreaterThanOrEqual(0);
|
|
160
|
+
|
|
161
|
+
const eventsAfterError = events.slice(errorIdx + 1);
|
|
162
|
+
const finishAfterError = eventsAfterError.filter(
|
|
163
|
+
(e) => e.type === EventType.RUN_FINISHED,
|
|
164
|
+
);
|
|
165
|
+
expect(finishAfterError.length).toBe(0);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// -------------------------------------------------------------------------
|
|
170
|
+
// Abort
|
|
171
|
+
// -------------------------------------------------------------------------
|
|
172
|
+
describe("abort", () => {
|
|
173
|
+
it("completes without error after abortRun()", async () => {
|
|
174
|
+
// Use a signal to synchronize: abort after the first chunk is emitted
|
|
175
|
+
let emittedFirstChunk: () => void;
|
|
176
|
+
const firstChunkEmitted = new Promise<void>(
|
|
177
|
+
(r) => (emittedFirstChunk = r),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
let config: BuiltInAgentFactoryConfig;
|
|
181
|
+
switch (type) {
|
|
182
|
+
case "aisdk":
|
|
183
|
+
config = {
|
|
184
|
+
type: "aisdk",
|
|
185
|
+
factory: ({ abortSignal }: AgentFactoryContext) => ({
|
|
186
|
+
fullStream: (async function* () {
|
|
187
|
+
yield { type: "text-delta", text: "tick" };
|
|
188
|
+
emittedFirstChunk();
|
|
189
|
+
// Wait for abort — use a promise that resolves on abort
|
|
190
|
+
await new Promise<void>((r) => {
|
|
191
|
+
if (abortSignal.aborted) return r();
|
|
192
|
+
abortSignal.addEventListener("abort", () => r(), {
|
|
193
|
+
once: true,
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
})(),
|
|
197
|
+
}),
|
|
198
|
+
};
|
|
199
|
+
break;
|
|
200
|
+
case "tanstack":
|
|
201
|
+
config = {
|
|
202
|
+
type: "tanstack",
|
|
203
|
+
factory: ({ abortSignal }: AgentFactoryContext) => ({
|
|
204
|
+
[Symbol.asyncIterator]: async function* () {
|
|
205
|
+
yield { type: "TEXT_MESSAGE_CONTENT", delta: "tick" };
|
|
206
|
+
emittedFirstChunk();
|
|
207
|
+
await new Promise<void>((r) => {
|
|
208
|
+
if (abortSignal.aborted) return r();
|
|
209
|
+
abortSignal.addEventListener("abort", () => r(), {
|
|
210
|
+
once: true,
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
},
|
|
214
|
+
}),
|
|
215
|
+
};
|
|
216
|
+
break;
|
|
217
|
+
case "custom":
|
|
218
|
+
config = {
|
|
219
|
+
type: "custom",
|
|
220
|
+
factory: ({ abortSignal }: AgentFactoryContext) => ({
|
|
221
|
+
[Symbol.asyncIterator]: async function* () {
|
|
222
|
+
yield {
|
|
223
|
+
type: EventType.TEXT_MESSAGE_CHUNK,
|
|
224
|
+
role: "assistant",
|
|
225
|
+
delta: "tick",
|
|
226
|
+
} as BaseEvent;
|
|
227
|
+
emittedFirstChunk();
|
|
228
|
+
await new Promise<void>((r) => {
|
|
229
|
+
if (abortSignal.aborted) return r();
|
|
230
|
+
abortSignal.addEventListener("abort", () => r(), {
|
|
231
|
+
once: true,
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
},
|
|
235
|
+
}),
|
|
236
|
+
};
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const agent = new BuiltInAgent(config);
|
|
241
|
+
const input = createDefaultInput();
|
|
242
|
+
|
|
243
|
+
const completed = await new Promise<boolean>((resolve) => {
|
|
244
|
+
agent.run(input).subscribe({
|
|
245
|
+
next: () => {},
|
|
246
|
+
error: () => resolve(false),
|
|
247
|
+
complete: () => resolve(true),
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Wait for the first chunk to be emitted, then abort
|
|
251
|
+
firstChunkEmitted.then(() => agent.abortRun());
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
expect(completed).toBe(true);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// -------------------------------------------------------------------------
|
|
259
|
+
// Factory Context
|
|
260
|
+
// -------------------------------------------------------------------------
|
|
261
|
+
describe("factory context", () => {
|
|
262
|
+
it("receives correct input with threadId, runId, and forwardedProps", async () => {
|
|
263
|
+
let capturedCtx: AgentFactoryContext | null = null;
|
|
264
|
+
|
|
265
|
+
let config: BuiltInAgentFactoryConfig;
|
|
266
|
+
switch (type) {
|
|
267
|
+
case "aisdk":
|
|
268
|
+
config = {
|
|
269
|
+
type: "aisdk",
|
|
270
|
+
factory: (ctx: AgentFactoryContext) => {
|
|
271
|
+
capturedCtx = ctx;
|
|
272
|
+
return {
|
|
273
|
+
fullStream: (async function* () {
|
|
274
|
+
yield { type: "finish", finishReason: "stop" };
|
|
275
|
+
})(),
|
|
276
|
+
};
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
break;
|
|
280
|
+
case "tanstack":
|
|
281
|
+
config = {
|
|
282
|
+
type: "tanstack",
|
|
283
|
+
factory: (ctx: AgentFactoryContext) => {
|
|
284
|
+
capturedCtx = ctx;
|
|
285
|
+
return (async function* () {
|
|
286
|
+
// empty stream
|
|
287
|
+
})();
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
break;
|
|
291
|
+
case "custom":
|
|
292
|
+
config = {
|
|
293
|
+
type: "custom",
|
|
294
|
+
factory: (ctx: AgentFactoryContext) => {
|
|
295
|
+
capturedCtx = ctx;
|
|
296
|
+
return (async function* () {
|
|
297
|
+
// empty stream
|
|
298
|
+
})();
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const agent = new BuiltInAgent(config);
|
|
305
|
+
const input = createDefaultInput({
|
|
306
|
+
threadId: "ctx-thread",
|
|
307
|
+
runId: "ctx-run",
|
|
308
|
+
forwardedProps: { model: "gpt-4" },
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
await collectEvents(agent.run(input));
|
|
312
|
+
|
|
313
|
+
expect(capturedCtx).not.toBeNull();
|
|
314
|
+
expect(capturedCtx!.input.threadId).toBe("ctx-thread");
|
|
315
|
+
expect(capturedCtx!.input.runId).toBe("ctx-run");
|
|
316
|
+
expect(capturedCtx!.input.forwardedProps).toEqual({ model: "gpt-4" });
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("receives abortController and abortSignal", async () => {
|
|
320
|
+
let capturedCtx: AgentFactoryContext | null = null;
|
|
321
|
+
|
|
322
|
+
let config: BuiltInAgentFactoryConfig;
|
|
323
|
+
switch (type) {
|
|
324
|
+
case "aisdk":
|
|
325
|
+
config = {
|
|
326
|
+
type: "aisdk",
|
|
327
|
+
factory: (ctx: AgentFactoryContext) => {
|
|
328
|
+
capturedCtx = ctx;
|
|
329
|
+
return {
|
|
330
|
+
fullStream: (async function* () {
|
|
331
|
+
yield { type: "finish", finishReason: "stop" };
|
|
332
|
+
})(),
|
|
333
|
+
};
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
break;
|
|
337
|
+
case "tanstack":
|
|
338
|
+
config = {
|
|
339
|
+
type: "tanstack",
|
|
340
|
+
factory: (ctx: AgentFactoryContext) => {
|
|
341
|
+
capturedCtx = ctx;
|
|
342
|
+
return (async function* () {
|
|
343
|
+
// empty
|
|
344
|
+
})();
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
break;
|
|
348
|
+
case "custom":
|
|
349
|
+
config = {
|
|
350
|
+
type: "custom",
|
|
351
|
+
factory: (ctx: AgentFactoryContext) => {
|
|
352
|
+
capturedCtx = ctx;
|
|
353
|
+
return (async function* () {
|
|
354
|
+
// empty
|
|
355
|
+
})();
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const agent = new BuiltInAgent(config);
|
|
362
|
+
const input = createDefaultInput();
|
|
363
|
+
await collectEvents(agent.run(input));
|
|
364
|
+
|
|
365
|
+
expect(capturedCtx!.abortController).toBeInstanceOf(AbortController);
|
|
366
|
+
expect(capturedCtx!.abortSignal).toBe(
|
|
367
|
+
capturedCtx!.abortController.signal,
|
|
368
|
+
);
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// -------------------------------------------------------------------------
|
|
373
|
+
// clone()
|
|
374
|
+
// -------------------------------------------------------------------------
|
|
375
|
+
describe("clone()", () => {
|
|
376
|
+
it("returns a new Agent instance (not the same reference)", () => {
|
|
377
|
+
const agent = createAgent(type, minimalStreamData(type));
|
|
378
|
+
const cloned = agent.clone();
|
|
379
|
+
|
|
380
|
+
expect(cloned).toBeInstanceOf(BuiltInAgent);
|
|
381
|
+
expect(cloned).not.toBe(agent);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it("produces correct lifecycle events from a cloned agent", async () => {
|
|
385
|
+
const agent = createAgent(type, minimalStreamData(type));
|
|
386
|
+
const cloned = agent.clone();
|
|
387
|
+
const input = createDefaultInput({
|
|
388
|
+
threadId: "clone-t",
|
|
389
|
+
runId: "clone-r",
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const events = await collectEvents(cloned.run(input));
|
|
393
|
+
|
|
394
|
+
expectLifecycleWrapped(events, "clone-t", "clone-r");
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
400
|
+
// Type Discrimination (NOT parameterized)
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
|
|
403
|
+
describe("Agent type discrimination", () => {
|
|
404
|
+
it('"aisdk" routes to AI SDK converter and produces text content', async () => {
|
|
405
|
+
const agent = createAgent("aisdk", [
|
|
406
|
+
textDelta("hello from aisdk"),
|
|
407
|
+
finish(),
|
|
408
|
+
]);
|
|
409
|
+
const input = createDefaultInput();
|
|
410
|
+
const events = await collectEvents(agent.run(input));
|
|
411
|
+
|
|
412
|
+
const textEvents = events.filter(
|
|
413
|
+
(e) => e.type === EventType.TEXT_MESSAGE_CHUNK,
|
|
414
|
+
);
|
|
415
|
+
expect(textEvents.length).toBe(1);
|
|
416
|
+
expect(eventField<string>(textEvents[0], "delta")).toBe("hello from aisdk");
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('"tanstack" routes to TanStack converter and produces text content', async () => {
|
|
420
|
+
const agent = createAgent("tanstack", [
|
|
421
|
+
tanstackTextChunk("hello from tanstack"),
|
|
422
|
+
]);
|
|
423
|
+
const input = createDefaultInput();
|
|
424
|
+
const events = await collectEvents(agent.run(input));
|
|
425
|
+
|
|
426
|
+
const textEvents = events.filter(
|
|
427
|
+
(e) => e.type === EventType.TEXT_MESSAGE_CHUNK,
|
|
428
|
+
);
|
|
429
|
+
expect(textEvents.length).toBe(1);
|
|
430
|
+
expect(eventField<string>(textEvents[0], "delta")).toBe(
|
|
431
|
+
"hello from tanstack",
|
|
432
|
+
);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('"custom" forwards events directly without conversion', async () => {
|
|
436
|
+
const customEvent: BaseEvent = {
|
|
437
|
+
type: EventType.TEXT_MESSAGE_CHUNK,
|
|
438
|
+
role: "assistant",
|
|
439
|
+
delta: "hello from custom",
|
|
440
|
+
} as BaseEvent;
|
|
441
|
+
|
|
442
|
+
const agent = createAgent("custom", [customEvent]);
|
|
443
|
+
const input = createDefaultInput();
|
|
444
|
+
const events = await collectEvents(agent.run(input));
|
|
445
|
+
|
|
446
|
+
const textEvents = events.filter(
|
|
447
|
+
(e) => e.type === EventType.TEXT_MESSAGE_CHUNK,
|
|
448
|
+
);
|
|
449
|
+
expect(textEvents.length).toBe(1);
|
|
450
|
+
expect(eventField<string>(textEvents[0], "delta")).toBe(
|
|
451
|
+
"hello from custom",
|
|
452
|
+
);
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// ---------------------------------------------------------------------------
|
|
457
|
+
// Async Factory (Promise-returning)
|
|
458
|
+
// ---------------------------------------------------------------------------
|
|
459
|
+
|
|
460
|
+
describe("Async factory (Promise-returning)", () => {
|
|
461
|
+
it("aisdk: async factory resolves and streams correctly", async () => {
|
|
462
|
+
const agent = new BuiltInAgent({
|
|
463
|
+
type: "aisdk",
|
|
464
|
+
factory: async () => {
|
|
465
|
+
// Simulate async setup (e.g., fetching API key)
|
|
466
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
467
|
+
return {
|
|
468
|
+
fullStream: (async function* () {
|
|
469
|
+
yield { type: "text-delta", text: "async-aisdk" };
|
|
470
|
+
yield { type: "finish", finishReason: "stop" };
|
|
471
|
+
})(),
|
|
472
|
+
};
|
|
473
|
+
},
|
|
474
|
+
});
|
|
475
|
+
const input = createDefaultInput();
|
|
476
|
+
const events = await collectEvents(agent.run(input));
|
|
477
|
+
|
|
478
|
+
expectLifecycleWrapped(events);
|
|
479
|
+
const textEvents = events.filter(
|
|
480
|
+
(e) => e.type === EventType.TEXT_MESSAGE_CHUNK,
|
|
481
|
+
);
|
|
482
|
+
expect(textEvents).toHaveLength(1);
|
|
483
|
+
expect(eventField<string>(textEvents[0], "delta")).toBe("async-aisdk");
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it("tanstack: async factory resolves and streams correctly", async () => {
|
|
487
|
+
const agent = new BuiltInAgent({
|
|
488
|
+
type: "tanstack",
|
|
489
|
+
factory: async () => {
|
|
490
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
491
|
+
return (async function* () {
|
|
492
|
+
yield { type: "TEXT_MESSAGE_CONTENT", delta: "async-tanstack" };
|
|
493
|
+
})();
|
|
494
|
+
},
|
|
495
|
+
});
|
|
496
|
+
const input = createDefaultInput();
|
|
497
|
+
const events = await collectEvents(agent.run(input));
|
|
498
|
+
|
|
499
|
+
expectLifecycleWrapped(events);
|
|
500
|
+
const textEvents = events.filter(
|
|
501
|
+
(e) => e.type === EventType.TEXT_MESSAGE_CHUNK,
|
|
502
|
+
);
|
|
503
|
+
expect(textEvents).toHaveLength(1);
|
|
504
|
+
expect(eventField<string>(textEvents[0], "delta")).toBe("async-tanstack");
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it("custom: async factory resolves and streams correctly", async () => {
|
|
508
|
+
const agent = new BuiltInAgent({
|
|
509
|
+
type: "custom",
|
|
510
|
+
factory: async () => {
|
|
511
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
512
|
+
return (async function* () {
|
|
513
|
+
yield {
|
|
514
|
+
type: EventType.TEXT_MESSAGE_CHUNK,
|
|
515
|
+
role: "assistant",
|
|
516
|
+
delta: "async-custom",
|
|
517
|
+
} as BaseEvent;
|
|
518
|
+
})();
|
|
519
|
+
},
|
|
520
|
+
});
|
|
521
|
+
const input = createDefaultInput();
|
|
522
|
+
const events = await collectEvents(agent.run(input));
|
|
523
|
+
|
|
524
|
+
expectLifecycleWrapped(events);
|
|
525
|
+
const textEvents = events.filter(
|
|
526
|
+
(e) => e.type === EventType.TEXT_MESSAGE_CHUNK,
|
|
527
|
+
);
|
|
528
|
+
expect(textEvents).toHaveLength(1);
|
|
529
|
+
expect(eventField<string>(textEvents[0], "delta")).toBe("async-custom");
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// ---------------------------------------------------------------------------
|
|
534
|
+
// RUN_ERROR includes threadId and runId
|
|
535
|
+
// ---------------------------------------------------------------------------
|
|
536
|
+
|
|
537
|
+
describe("RUN_ERROR correlation fields", () => {
|
|
538
|
+
it("RUN_ERROR includes threadId and runId for run correlation", async () => {
|
|
539
|
+
const agent = new BuiltInAgent({
|
|
540
|
+
type: "aisdk",
|
|
541
|
+
factory: () => {
|
|
542
|
+
throw new Error("test-error");
|
|
543
|
+
},
|
|
544
|
+
});
|
|
545
|
+
const input = createDefaultInput({
|
|
546
|
+
threadId: "err-thread",
|
|
547
|
+
runId: "err-run",
|
|
548
|
+
});
|
|
549
|
+
const { events, errored } = await collectEventsIncludingErrors(
|
|
550
|
+
agent.run(input),
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
expect(errored).toBe(true);
|
|
554
|
+
const errorEvents = events.filter((e) => e.type === EventType.RUN_ERROR);
|
|
555
|
+
expect(errorEvents).toHaveLength(1);
|
|
556
|
+
expect(eventField<string>(errorEvents[0], "threadId")).toBe("err-thread");
|
|
557
|
+
expect(eventField<string>(errorEvents[0], "runId")).toBe("err-run");
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// ---------------------------------------------------------------------------
|
|
562
|
+
// Concurrent run guard
|
|
563
|
+
// ---------------------------------------------------------------------------
|
|
564
|
+
|
|
565
|
+
describe("Concurrent run guard", () => {
|
|
566
|
+
it("throws when run() is called while another run is in progress", async () => {
|
|
567
|
+
let resolveFactory: () => void;
|
|
568
|
+
const factoryBlocked = new Promise<void>((r) => (resolveFactory = r));
|
|
569
|
+
|
|
570
|
+
const agent = new BuiltInAgent({
|
|
571
|
+
type: "custom",
|
|
572
|
+
factory: async function* ({ abortSignal }) {
|
|
573
|
+
// Block until resolved externally
|
|
574
|
+
await new Promise<void>((r) => {
|
|
575
|
+
if (abortSignal.aborted) return r();
|
|
576
|
+
abortSignal.addEventListener("abort", () => r(), { once: true });
|
|
577
|
+
factoryBlocked.then(() => r());
|
|
578
|
+
});
|
|
579
|
+
},
|
|
580
|
+
});
|
|
581
|
+
const input = createDefaultInput();
|
|
582
|
+
|
|
583
|
+
// Start first run — abortController is now set synchronously in run()
|
|
584
|
+
const sub = agent.run(input).subscribe({ next: () => {} });
|
|
585
|
+
|
|
586
|
+
// Second run should throw immediately (no timing dependency)
|
|
587
|
+
expect(() => agent.run(input)).toThrow("Agent is already running");
|
|
588
|
+
|
|
589
|
+
// Cleanup
|
|
590
|
+
resolveFactory!();
|
|
591
|
+
sub.unsubscribe();
|
|
592
|
+
});
|
|
593
|
+
});
|