@copilotkitnext/agent 0.0.0-max-changeset-20260109174803

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/src/index.ts ADDED
@@ -0,0 +1,1003 @@
1
+ import {
2
+ AbstractAgent,
3
+ BaseEvent,
4
+ RunAgentInput,
5
+ EventType,
6
+ Message,
7
+ RunFinishedEvent,
8
+ RunStartedEvent,
9
+ TextMessageChunkEvent,
10
+ ToolCallArgsEvent,
11
+ ToolCallEndEvent,
12
+ ToolCallStartEvent,
13
+ ToolCallResultEvent,
14
+ RunErrorEvent,
15
+ StateSnapshotEvent,
16
+ StateDeltaEvent,
17
+ } from "@ag-ui/client";
18
+ import {
19
+ streamText,
20
+ LanguageModel,
21
+ ModelMessage,
22
+ AssistantModelMessage,
23
+ UserModelMessage,
24
+ ToolModelMessage,
25
+ ToolCallPart,
26
+ ToolResultPart,
27
+ TextPart,
28
+ tool as createVercelAISDKTool,
29
+ ToolChoice,
30
+ ToolSet,
31
+ stepCountIs,
32
+ } from "ai";
33
+ import { experimental_createMCPClient as createMCPClient } from "@ai-sdk/mcp";
34
+ import { Observable } from "rxjs";
35
+ import { createOpenAI } from "@ai-sdk/openai";
36
+ import { createAnthropic } from "@ai-sdk/anthropic";
37
+ import { createGoogleGenerativeAI } from "@ai-sdk/google";
38
+ import { randomUUID } from "crypto";
39
+ import { z } from "zod";
40
+ import {
41
+ StreamableHTTPClientTransport,
42
+ StreamableHTTPClientTransportOptions,
43
+ } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
44
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
45
+
46
+ /**
47
+ * Properties that can be overridden by forwardedProps
48
+ * These match the exact parameter names in streamText
49
+ */
50
+ export type OverridableProperty =
51
+ | "model"
52
+ | "toolChoice"
53
+ | "maxOutputTokens"
54
+ | "temperature"
55
+ | "topP"
56
+ | "topK"
57
+ | "presencePenalty"
58
+ | "frequencyPenalty"
59
+ | "stopSequences"
60
+ | "seed"
61
+ | "maxRetries"
62
+ | "prompt";
63
+
64
+ /**
65
+ * Supported model identifiers for BuiltInAgent
66
+ */
67
+ export type BuiltInAgentModel =
68
+ // OpenAI models
69
+ | "openai/gpt-5"
70
+ | "openai/gpt-5-mini"
71
+ | "openai/gpt-4.1"
72
+ | "openai/gpt-4.1-mini"
73
+ | "openai/gpt-4.1-nano"
74
+ | "openai/gpt-4o"
75
+ | "openai/gpt-4o-mini"
76
+ // OpenAI reasoning series
77
+ | "openai/o3"
78
+ | "openai/o3-mini"
79
+ | "openai/o4-mini"
80
+ // Anthropic (Claude) models
81
+ | "anthropic/claude-sonnet-4.5"
82
+ | "anthropic/claude-sonnet-4"
83
+ | "anthropic/claude-3.7-sonnet"
84
+ | "anthropic/claude-opus-4.1"
85
+ | "anthropic/claude-opus-4"
86
+ | "anthropic/claude-3.5-haiku"
87
+ // Google (Gemini) models
88
+ | "google/gemini-2.5-pro"
89
+ | "google/gemini-2.5-flash"
90
+ | "google/gemini-2.5-flash-lite"
91
+ // Allow any LanguageModel instance
92
+ | (string & {});
93
+
94
+ /**
95
+ * Model specifier - can be a string like "openai/gpt-4o" or a LanguageModel instance
96
+ */
97
+ export type ModelSpecifier = string | LanguageModel;
98
+
99
+ /**
100
+ * MCP Client configuration for HTTP transport
101
+ */
102
+ export interface MCPClientConfigHTTP {
103
+ /**
104
+ * Type of MCP client
105
+ */
106
+ type: "http";
107
+ /**
108
+ * URL of the MCP server
109
+ */
110
+ url: string;
111
+ /**
112
+ * Optional transport options for HTTP client
113
+ */
114
+ options?: StreamableHTTPClientTransportOptions;
115
+ }
116
+
117
+ /**
118
+ * MCP Client configuration for SSE transport
119
+ */
120
+ export interface MCPClientConfigSSE {
121
+ /**
122
+ * Type of MCP client
123
+ */
124
+ type: "sse";
125
+ /**
126
+ * URL of the MCP server
127
+ */
128
+ url: string;
129
+ /**
130
+ * Optional HTTP headers (e.g., for authentication)
131
+ */
132
+ headers?: Record<string, string>;
133
+ }
134
+
135
+ /**
136
+ * MCP Client configuration
137
+ */
138
+ export type MCPClientConfig = MCPClientConfigHTTP | MCPClientConfigSSE;
139
+
140
+ /**
141
+ * Resolves a model specifier to a LanguageModel instance
142
+ * @param spec - Model string (e.g., "openai/gpt-4o") or LanguageModel instance
143
+ * @returns LanguageModel instance
144
+ */
145
+ export function resolveModel(spec: ModelSpecifier): LanguageModel {
146
+ // If already a LanguageModel instance, pass through
147
+ if (typeof spec !== "string") {
148
+ return spec;
149
+ }
150
+
151
+ // Normalize "provider/model" or "provider:model" format
152
+ const normalized = spec.replace("/", ":").trim();
153
+ const parts = normalized.split(":");
154
+ const rawProvider = parts[0];
155
+ const rest = parts.slice(1);
156
+
157
+ if (!rawProvider) {
158
+ throw new Error(
159
+ `Invalid model string "${spec}". Use "openai/gpt-5", "anthropic/claude-sonnet-4.5", or "google/gemini-2.5-pro".`,
160
+ );
161
+ }
162
+
163
+ const provider = rawProvider.toLowerCase();
164
+ const model = rest.join(":").trim();
165
+
166
+ if (!model) {
167
+ throw new Error(
168
+ `Invalid model string "${spec}". Use "openai/gpt-5", "anthropic/claude-sonnet-4.5", or "google/gemini-2.5-pro".`,
169
+ );
170
+ }
171
+
172
+ switch (provider) {
173
+ case "openai": {
174
+ // Lazily create OpenAI provider
175
+ const openai = createOpenAI({
176
+ apiKey: process.env.OPENAI_API_KEY!,
177
+ });
178
+ // Accepts any OpenAI model id, e.g. "gpt-4o", "gpt-4.1-mini", "o3-mini"
179
+ return openai(model);
180
+ }
181
+
182
+ case "anthropic": {
183
+ // Lazily create Anthropic provider
184
+ const anthropic = createAnthropic({
185
+ apiKey: process.env.ANTHROPIC_API_KEY!,
186
+ });
187
+ // Accepts any Claude id, e.g. "claude-3.7-sonnet", "claude-3.5-haiku"
188
+ return anthropic(model);
189
+ }
190
+
191
+ case "google":
192
+ case "gemini":
193
+ case "google-gemini": {
194
+ // Lazily create Google provider
195
+ const google = createGoogleGenerativeAI({
196
+ apiKey: process.env.GOOGLE_API_KEY!,
197
+ });
198
+ // Accepts any Gemini id, e.g. "gemini-2.5-pro", "gemini-2.5-flash"
199
+ return google(model);
200
+ }
201
+
202
+ default:
203
+ throw new Error(`Unknown provider "${provider}" in "${spec}". Supported: openai, anthropic, google (gemini).`);
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Tool definition for BuiltInAgent
209
+ */
210
+ export interface ToolDefinition<TParameters extends z.ZodTypeAny = z.ZodTypeAny> {
211
+ name: string;
212
+ description: string;
213
+ parameters: TParameters;
214
+ execute: (args: z.infer<TParameters>) => Promise<unknown>;
215
+ }
216
+
217
+ /**
218
+ * Define a tool for use with BuiltInAgent
219
+ * @param name - The name of the tool
220
+ * @param description - Description of what the tool does
221
+ * @param parameters - Zod schema for the tool's input parameters
222
+ * @param execute - Function to execute the tool server-side
223
+ * @returns Tool definition
224
+ */
225
+ export function defineTool<TParameters extends z.ZodTypeAny>(config: {
226
+ name: string;
227
+ description: string;
228
+ parameters: TParameters;
229
+ execute: (args: z.infer<TParameters>) => Promise<unknown>;
230
+ }): ToolDefinition<TParameters> {
231
+ return {
232
+ name: config.name,
233
+ description: config.description,
234
+ parameters: config.parameters,
235
+ execute: config.execute,
236
+ };
237
+ }
238
+
239
+ type AGUIUserMessage = Extract<Message, { role: "user" }>;
240
+
241
+ function flattenUserMessageContent(content?: AGUIUserMessage["content"]): string {
242
+ if (!content) {
243
+ return "";
244
+ }
245
+
246
+ if (typeof content === "string") {
247
+ return content;
248
+ }
249
+
250
+ return content
251
+ .map((part) => {
252
+ if (
253
+ part &&
254
+ typeof part === "object" &&
255
+ "type" in part &&
256
+ (part as { type?: unknown }).type === "text" &&
257
+ typeof (part as { text?: unknown }).text === "string"
258
+ ) {
259
+ return (part as { text: string }).text;
260
+ }
261
+ return "";
262
+ })
263
+ .filter((text) => text.length > 0)
264
+ .join("\n");
265
+ }
266
+
267
+ /**
268
+ * Converts AG-UI messages to Vercel AI SDK ModelMessage format
269
+ */
270
+ export function convertMessagesToVercelAISDKMessages(messages: Message[]): ModelMessage[] {
271
+ const result: ModelMessage[] = [];
272
+
273
+ for (const message of messages) {
274
+ if (message.role === "assistant") {
275
+ const parts: Array<TextPart | ToolCallPart> = message.content ? [{ type: "text", text: message.content }] : [];
276
+
277
+ for (const toolCall of message.toolCalls ?? []) {
278
+ const toolCallPart: ToolCallPart = {
279
+ type: "tool-call",
280
+ toolCallId: toolCall.id,
281
+ toolName: toolCall.function.name,
282
+ input: JSON.parse(toolCall.function.arguments),
283
+ };
284
+ parts.push(toolCallPart);
285
+ }
286
+
287
+ const assistantMsg: AssistantModelMessage = {
288
+ role: "assistant",
289
+ content: parts,
290
+ };
291
+ result.push(assistantMsg);
292
+ } else if (message.role === "user") {
293
+ const userMsg: UserModelMessage = {
294
+ role: "user",
295
+ content: flattenUserMessageContent(message.content),
296
+ };
297
+ result.push(userMsg);
298
+ } else if (message.role === "tool") {
299
+ let toolName = "unknown";
300
+ // Find the tool name from the corresponding tool call
301
+ for (const msg of messages) {
302
+ if (msg.role === "assistant") {
303
+ for (const toolCall of msg.toolCalls ?? []) {
304
+ if (toolCall.id === message.toolCallId) {
305
+ toolName = toolCall.function.name;
306
+ break;
307
+ }
308
+ }
309
+ }
310
+ }
311
+
312
+ const toolResultPart: ToolResultPart = {
313
+ type: "tool-result",
314
+ toolCallId: message.toolCallId,
315
+ toolName: toolName,
316
+ output: {
317
+ type: "text",
318
+ value: message.content,
319
+ },
320
+ };
321
+
322
+ const toolMsg: ToolModelMessage = {
323
+ role: "tool",
324
+ content: [toolResultPart],
325
+ };
326
+ result.push(toolMsg);
327
+ }
328
+ }
329
+
330
+ return result;
331
+ }
332
+
333
+ /**
334
+ * JSON Schema type definition
335
+ */
336
+ interface JsonSchema {
337
+ type: "object" | "string" | "number" | "boolean" | "array";
338
+ description?: string;
339
+ properties?: Record<string, JsonSchema>;
340
+ required?: string[];
341
+ items?: JsonSchema;
342
+ }
343
+
344
+ /**
345
+ * Converts JSON Schema to Zod schema
346
+ */
347
+ export function convertJsonSchemaToZodSchema(jsonSchema: JsonSchema, required: boolean): z.ZodSchema {
348
+ if (jsonSchema.type === "object") {
349
+ const spec: { [key: string]: z.ZodSchema } = {};
350
+
351
+ if (!jsonSchema.properties || !Object.keys(jsonSchema.properties).length) {
352
+ return !required ? z.object(spec).optional() : z.object(spec);
353
+ }
354
+
355
+ for (const [key, value] of Object.entries(jsonSchema.properties)) {
356
+ spec[key] = convertJsonSchemaToZodSchema(value, jsonSchema.required ? jsonSchema.required.includes(key) : false);
357
+ }
358
+ let schema = z.object(spec).describe(jsonSchema.description ?? "");
359
+ return required ? schema : schema.optional();
360
+ } else if (jsonSchema.type === "string") {
361
+ let schema = z.string().describe(jsonSchema.description ?? "");
362
+ return required ? schema : schema.optional();
363
+ } else if (jsonSchema.type === "number") {
364
+ let schema = z.number().describe(jsonSchema.description ?? "");
365
+ return required ? schema : schema.optional();
366
+ } else if (jsonSchema.type === "boolean") {
367
+ let schema = z.boolean().describe(jsonSchema.description ?? "");
368
+ return required ? schema : schema.optional();
369
+ } else if (jsonSchema.type === "array") {
370
+ if (!jsonSchema.items) {
371
+ throw new Error("Array type must have items property");
372
+ }
373
+ let itemSchema = convertJsonSchemaToZodSchema(jsonSchema.items, true);
374
+ let schema = z.array(itemSchema).describe(jsonSchema.description ?? "");
375
+ return required ? schema : schema.optional();
376
+ }
377
+ throw new Error("Invalid JSON schema");
378
+ }
379
+
380
+ /**
381
+ * Converts AG-UI tools to Vercel AI SDK ToolSet
382
+ */
383
+ function isJsonSchema(obj: unknown): obj is JsonSchema {
384
+ if (typeof obj !== "object" || obj === null) return false;
385
+ const schema = obj as Record<string, unknown>;
386
+ return typeof schema.type === "string" && ["object", "string", "number", "boolean", "array"].includes(schema.type);
387
+ }
388
+
389
+ export function convertToolsToVercelAITools(tools: RunAgentInput["tools"]): ToolSet {
390
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
391
+ const result: Record<string, any> = {};
392
+
393
+ for (const tool of tools) {
394
+ if (!isJsonSchema(tool.parameters)) {
395
+ throw new Error(`Invalid JSON schema for tool ${tool.name}`);
396
+ }
397
+ const zodSchema = convertJsonSchemaToZodSchema(tool.parameters, true);
398
+ result[tool.name] = createVercelAISDKTool({
399
+ description: tool.description,
400
+ inputSchema: zodSchema,
401
+ });
402
+ }
403
+
404
+ return result;
405
+ }
406
+
407
+ /**
408
+ * Converts ToolDefinition array to Vercel AI SDK ToolSet
409
+ */
410
+ export function convertToolDefinitionsToVercelAITools(tools: ToolDefinition[]): ToolSet {
411
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
412
+ const result: Record<string, any> = {};
413
+
414
+ for (const tool of tools) {
415
+ result[tool.name] = createVercelAISDKTool({
416
+ description: tool.description,
417
+ inputSchema: tool.parameters,
418
+ execute: tool.execute,
419
+ });
420
+ }
421
+
422
+ return result;
423
+ }
424
+
425
+ /**
426
+ * Configuration for BuiltInAgent
427
+ */
428
+ export interface BuiltInAgentConfiguration {
429
+ /**
430
+ * The model to use
431
+ */
432
+ model: BuiltInAgentModel | LanguageModel;
433
+ /**
434
+ * Maximum number of steps/iterations for tool calling (default: 1)
435
+ */
436
+ maxSteps?: number;
437
+ /**
438
+ * Tool choice setting - how tools are selected for execution (default: "auto")
439
+ */
440
+ toolChoice?: ToolChoice<Record<string, unknown>>;
441
+ /**
442
+ * Maximum number of tokens to generate
443
+ */
444
+ maxOutputTokens?: number;
445
+ /**
446
+ * Temperature setting (range depends on provider)
447
+ */
448
+ temperature?: number;
449
+ /**
450
+ * Nucleus sampling (topP)
451
+ */
452
+ topP?: number;
453
+ /**
454
+ * Top K sampling
455
+ */
456
+ topK?: number;
457
+ /**
458
+ * Presence penalty
459
+ */
460
+ presencePenalty?: number;
461
+ /**
462
+ * Frequency penalty
463
+ */
464
+ frequencyPenalty?: number;
465
+ /**
466
+ * Sequences that will stop the generation
467
+ */
468
+ stopSequences?: string[];
469
+ /**
470
+ * Seed for deterministic results
471
+ */
472
+ seed?: number;
473
+ /**
474
+ * Maximum number of retries
475
+ */
476
+ maxRetries?: number;
477
+ /**
478
+ * Prompt for the agent
479
+ */
480
+ prompt?: string;
481
+ /**
482
+ * List of properties that can be overridden by forwardedProps.
483
+ */
484
+ overridableProperties?: OverridableProperty[];
485
+ /**
486
+ * Optional list of MCP server configurations
487
+ */
488
+ mcpServers?: MCPClientConfig[];
489
+ /**
490
+ * Optional tools available to the agent
491
+ */
492
+ tools?: ToolDefinition[];
493
+ }
494
+
495
+ export class BuiltInAgent extends AbstractAgent {
496
+ private abortController?: AbortController;
497
+
498
+ constructor(private config: BuiltInAgentConfiguration) {
499
+ super();
500
+ }
501
+
502
+ /**
503
+ * Check if a property can be overridden by forwardedProps
504
+ */
505
+ canOverride(property: OverridableProperty): boolean {
506
+ return this.config?.overridableProperties?.includes(property) ?? false;
507
+ }
508
+
509
+ run(input: RunAgentInput): Observable<BaseEvent> {
510
+ return new Observable<BaseEvent>((subscriber) => {
511
+ // Emit RUN_STARTED event
512
+ const startEvent: RunStartedEvent = {
513
+ type: EventType.RUN_STARTED,
514
+ threadId: input.threadId,
515
+ runId: input.runId,
516
+ };
517
+ subscriber.next(startEvent);
518
+
519
+ // Resolve the model
520
+ const model = resolveModel(this.config.model);
521
+
522
+ // Build prompt based on conditions
523
+ let systemPrompt: string | undefined = undefined;
524
+
525
+ // Check if we should build a prompt:
526
+ // - config.prompt is set, OR
527
+ // - input.context is non-empty, OR
528
+ // - input.state is non-empty and not an empty object
529
+ const hasPrompt = !!this.config.prompt;
530
+ const hasContext = input.context && input.context.length > 0;
531
+ const hasState =
532
+ input.state !== undefined &&
533
+ input.state !== null &&
534
+ !(typeof input.state === "object" && Object.keys(input.state).length === 0);
535
+
536
+ if (hasPrompt || hasContext || hasState) {
537
+ const parts: string[] = [];
538
+
539
+ // First: the prompt if any
540
+ if (hasPrompt) {
541
+ parts.push(this.config.prompt!);
542
+ }
543
+
544
+ // Second: context from the application
545
+ if (hasContext) {
546
+ parts.push("\n## Context from the application\n");
547
+ for (const ctx of input.context) {
548
+ parts.push(`${ctx.description}:\n${ctx.value}\n`);
549
+ }
550
+ }
551
+
552
+ // Third: state from the application that can be edited
553
+ if (hasState) {
554
+ parts.push(
555
+ "\n## Application State\n" +
556
+ "This is state from the application that you can edit by calling AGUISendStateSnapshot or AGUISendStateDelta.\n" +
557
+ `\`\`\`json\n${JSON.stringify(input.state, null, 2)}\n\`\`\`\n`,
558
+ );
559
+ }
560
+
561
+ systemPrompt = parts.join("");
562
+ }
563
+
564
+ // Convert messages and prepend system message if we have a prompt
565
+ const messages = convertMessagesToVercelAISDKMessages(input.messages);
566
+ if (systemPrompt) {
567
+ messages.unshift({
568
+ role: "system",
569
+ content: systemPrompt,
570
+ });
571
+ }
572
+
573
+ // Merge tools from input and config
574
+ let allTools: ToolSet = convertToolsToVercelAITools(input.tools);
575
+ if (this.config.tools && this.config.tools.length > 0) {
576
+ const configTools = convertToolDefinitionsToVercelAITools(this.config.tools);
577
+ allTools = { ...allTools, ...configTools };
578
+ }
579
+
580
+ const streamTextParams: Parameters<typeof streamText>[0] = {
581
+ model,
582
+ messages,
583
+ tools: allTools,
584
+ toolChoice: this.config.toolChoice,
585
+ stopWhen: this.config.maxSteps ? stepCountIs(this.config.maxSteps) : undefined,
586
+ maxOutputTokens: this.config.maxOutputTokens,
587
+ temperature: this.config.temperature,
588
+ topP: this.config.topP,
589
+ topK: this.config.topK,
590
+ presencePenalty: this.config.presencePenalty,
591
+ frequencyPenalty: this.config.frequencyPenalty,
592
+ stopSequences: this.config.stopSequences,
593
+ seed: this.config.seed,
594
+ maxRetries: this.config.maxRetries,
595
+ };
596
+
597
+ // Apply forwardedProps overrides (if allowed)
598
+ if (input.forwardedProps && typeof input.forwardedProps === "object") {
599
+ const props = input.forwardedProps as Record<string, unknown>;
600
+
601
+ // Check and apply each overridable property
602
+ if (props.model !== undefined && this.canOverride("model")) {
603
+ if (typeof props.model === "string" || typeof props.model === "object") {
604
+ // Accept any string or LanguageModel instance for model override
605
+ streamTextParams.model = resolveModel(props.model as string | LanguageModel);
606
+ }
607
+ }
608
+ if (props.toolChoice !== undefined && this.canOverride("toolChoice")) {
609
+ // ToolChoice can be 'auto', 'required', 'none', or { type: 'tool', toolName: string }
610
+ const toolChoice = props.toolChoice;
611
+ if (
612
+ toolChoice === "auto" ||
613
+ toolChoice === "required" ||
614
+ toolChoice === "none" ||
615
+ (typeof toolChoice === "object" &&
616
+ toolChoice !== null &&
617
+ "type" in toolChoice &&
618
+ toolChoice.type === "tool")
619
+ ) {
620
+ streamTextParams.toolChoice = toolChoice as ToolChoice<Record<string, unknown>>;
621
+ }
622
+ }
623
+ if (typeof props.maxOutputTokens === "number" && this.canOverride("maxOutputTokens")) {
624
+ streamTextParams.maxOutputTokens = props.maxOutputTokens;
625
+ }
626
+ if (typeof props.temperature === "number" && this.canOverride("temperature")) {
627
+ streamTextParams.temperature = props.temperature;
628
+ }
629
+ if (typeof props.topP === "number" && this.canOverride("topP")) {
630
+ streamTextParams.topP = props.topP;
631
+ }
632
+ if (typeof props.topK === "number" && this.canOverride("topK")) {
633
+ streamTextParams.topK = props.topK;
634
+ }
635
+ if (typeof props.presencePenalty === "number" && this.canOverride("presencePenalty")) {
636
+ streamTextParams.presencePenalty = props.presencePenalty;
637
+ }
638
+ if (typeof props.frequencyPenalty === "number" && this.canOverride("frequencyPenalty")) {
639
+ streamTextParams.frequencyPenalty = props.frequencyPenalty;
640
+ }
641
+ if (Array.isArray(props.stopSequences) && this.canOverride("stopSequences")) {
642
+ // Validate all elements are strings
643
+ if (props.stopSequences.every((item): item is string => typeof item === "string")) {
644
+ streamTextParams.stopSequences = props.stopSequences;
645
+ }
646
+ }
647
+ if (typeof props.seed === "number" && this.canOverride("seed")) {
648
+ streamTextParams.seed = props.seed;
649
+ }
650
+ if (typeof props.maxRetries === "number" && this.canOverride("maxRetries")) {
651
+ streamTextParams.maxRetries = props.maxRetries;
652
+ }
653
+ }
654
+
655
+ // Set up MCP clients if configured and process the stream
656
+ const mcpClients: Array<{ close: () => Promise<void> }> = [];
657
+
658
+ (async () => {
659
+ const abortController = new AbortController();
660
+ this.abortController = abortController;
661
+ let terminalEventEmitted = false;
662
+
663
+ try {
664
+ // Add AG-UI state update tools
665
+ streamTextParams.tools = {
666
+ ...streamTextParams.tools,
667
+ AGUISendStateSnapshot: createVercelAISDKTool({
668
+ description: "Replace the entire application state with a new snapshot",
669
+ inputSchema: z.object({
670
+ snapshot: z.any().describe("The complete new state object"),
671
+ }),
672
+ execute: async ({ snapshot }) => {
673
+ return { success: true, snapshot };
674
+ },
675
+ }),
676
+ AGUISendStateDelta: createVercelAISDKTool({
677
+ description: "Apply incremental updates to application state using JSON Patch operations",
678
+ inputSchema: z.object({
679
+ delta: z
680
+ .array(
681
+ z.object({
682
+ op: z.enum(["add", "replace", "remove"]).describe("The operation to perform"),
683
+ path: z.string().describe("JSON Pointer path (e.g., '/foo/bar')"),
684
+ value: z
685
+ .any()
686
+ .optional()
687
+ .describe(
688
+ "The value to set. Required for 'add' and 'replace' operations, ignored for 'remove'.",
689
+ ),
690
+ }),
691
+ )
692
+ .describe("Array of JSON Patch operations"),
693
+ }),
694
+ execute: async ({ delta }) => {
695
+ return { success: true, delta };
696
+ },
697
+ }),
698
+ };
699
+
700
+ // Initialize MCP clients and get their tools
701
+ if (this.config.mcpServers && this.config.mcpServers.length > 0) {
702
+ for (const serverConfig of this.config.mcpServers) {
703
+ let transport;
704
+
705
+ if (serverConfig.type === "http") {
706
+ const url = new URL(serverConfig.url);
707
+ transport = new StreamableHTTPClientTransport(url, serverConfig.options);
708
+ } else if (serverConfig.type === "sse") {
709
+ transport = new SSEClientTransport(new URL(serverConfig.url), serverConfig.headers);
710
+ }
711
+
712
+ if (transport) {
713
+ const mcpClient = await createMCPClient({ transport });
714
+ mcpClients.push(mcpClient);
715
+
716
+ // Get tools from this MCP server and merge with existing tools
717
+ const mcpTools = await mcpClient.tools();
718
+ streamTextParams.tools = { ...streamTextParams.tools, ...mcpTools } as ToolSet;
719
+ }
720
+ }
721
+ }
722
+
723
+ // Call streamText and process the stream
724
+ const response = streamText({ ...streamTextParams, abortSignal: abortController.signal });
725
+
726
+ let messageId = randomUUID();
727
+
728
+ const toolCallStates = new Map<
729
+ string,
730
+ {
731
+ started: boolean;
732
+ hasArgsDelta: boolean;
733
+ ended: boolean;
734
+ toolName?: string;
735
+ }
736
+ >();
737
+
738
+ const ensureToolCallState = (toolCallId: string) => {
739
+ let state = toolCallStates.get(toolCallId);
740
+ if (!state) {
741
+ state = { started: false, hasArgsDelta: false, ended: false };
742
+ toolCallStates.set(toolCallId, state);
743
+ }
744
+ return state;
745
+ };
746
+
747
+ // Process fullStream events
748
+ for await (const part of response.fullStream) {
749
+ switch (part.type) {
750
+ case 'abort':
751
+ const abortEndEvent: RunFinishedEvent = {
752
+ type: EventType.RUN_FINISHED,
753
+ threadId: input.threadId,
754
+ runId: input.runId,
755
+ };
756
+ subscriber.next(abortEndEvent);
757
+ terminalEventEmitted = true;
758
+
759
+ // Complete the observable
760
+ subscriber.complete();
761
+ break;
762
+
763
+ case "tool-input-start": {
764
+ const toolCallId = part.id;
765
+ const state = ensureToolCallState(toolCallId);
766
+ state.toolName = part.toolName;
767
+ if (!state.started) {
768
+ state.started = true;
769
+ const startEvent: ToolCallStartEvent = {
770
+ type: EventType.TOOL_CALL_START,
771
+ parentMessageId: messageId,
772
+ toolCallId,
773
+ toolCallName: part.toolName,
774
+ };
775
+ subscriber.next(startEvent);
776
+ }
777
+ break;
778
+ }
779
+
780
+ case "tool-input-delta": {
781
+ const toolCallId = part.id;
782
+ const state = ensureToolCallState(toolCallId);
783
+ state.hasArgsDelta = true;
784
+ const argsEvent: ToolCallArgsEvent = {
785
+ type: EventType.TOOL_CALL_ARGS,
786
+ toolCallId,
787
+ delta: part.delta,
788
+ };
789
+ subscriber.next(argsEvent);
790
+ break;
791
+ }
792
+
793
+ case "tool-input-end": {
794
+ // No direct event – the subsequent "tool-call" part marks completion.
795
+ break;
796
+ }
797
+
798
+ case "text-start": {
799
+ // New text message starting - use the SDK-provided id
800
+ // Use randomUUID() if part.id is falsy or "0" to prevent message merging issues
801
+ const providedId = "id" in part ? part.id : undefined;
802
+ messageId = (providedId && providedId !== "0") ? (providedId as typeof messageId) : randomUUID();
803
+ break;
804
+ }
805
+
806
+ case "text-delta": {
807
+ // Accumulate text content - in AI SDK 5.0, the property is 'text'
808
+ const textDelta = "text" in part ? part.text : "";
809
+ // Emit text chunk event
810
+ const textEvent: TextMessageChunkEvent = {
811
+ type: EventType.TEXT_MESSAGE_CHUNK,
812
+ role: "assistant",
813
+ messageId,
814
+ delta: textDelta,
815
+ };
816
+ subscriber.next(textEvent);
817
+ break;
818
+ }
819
+
820
+ case "tool-call": {
821
+ const toolCallId = part.toolCallId;
822
+ const state = ensureToolCallState(toolCallId);
823
+ state.toolName = part.toolName ?? state.toolName;
824
+
825
+ if (!state.started) {
826
+ state.started = true;
827
+ const startEvent: ToolCallStartEvent = {
828
+ type: EventType.TOOL_CALL_START,
829
+ parentMessageId: messageId,
830
+ toolCallId,
831
+ toolCallName: part.toolName,
832
+ };
833
+ subscriber.next(startEvent);
834
+ }
835
+
836
+ if (!state.hasArgsDelta && "input" in part && part.input !== undefined) {
837
+ let serializedInput = "";
838
+ if (typeof part.input === "string") {
839
+ serializedInput = part.input;
840
+ } else {
841
+ try {
842
+ serializedInput = JSON.stringify(part.input);
843
+ } catch {
844
+ serializedInput = String(part.input);
845
+ }
846
+ }
847
+
848
+ if (serializedInput.length > 0) {
849
+ const argsEvent: ToolCallArgsEvent = {
850
+ type: EventType.TOOL_CALL_ARGS,
851
+ toolCallId,
852
+ delta: serializedInput,
853
+ };
854
+ subscriber.next(argsEvent);
855
+ state.hasArgsDelta = true;
856
+ }
857
+ }
858
+
859
+ if (!state.ended) {
860
+ state.ended = true;
861
+ const endEvent: ToolCallEndEvent = {
862
+ type: EventType.TOOL_CALL_END,
863
+ toolCallId,
864
+ };
865
+ subscriber.next(endEvent);
866
+ }
867
+ break;
868
+ }
869
+
870
+ case "tool-result": {
871
+ const toolResult = "output" in part ? part.output : null;
872
+ const toolName = "toolName" in part ? part.toolName : "";
873
+ toolCallStates.delete(part.toolCallId);
874
+
875
+ // Check if this is a state update tool
876
+ if (toolName === "AGUISendStateSnapshot" && toolResult && typeof toolResult === "object") {
877
+ // Emit StateSnapshotEvent
878
+ const stateSnapshotEvent: StateSnapshotEvent = {
879
+ type: EventType.STATE_SNAPSHOT,
880
+ snapshot: toolResult.snapshot,
881
+ };
882
+ subscriber.next(stateSnapshotEvent);
883
+ } else if (toolName === "AGUISendStateDelta" && toolResult && typeof toolResult === "object") {
884
+ // Emit StateDeltaEvent
885
+ const stateDeltaEvent: StateDeltaEvent = {
886
+ type: EventType.STATE_DELTA,
887
+ delta: toolResult.delta,
888
+ };
889
+ subscriber.next(stateDeltaEvent);
890
+ }
891
+
892
+ // Always emit the tool result event for the LLM
893
+ const resultEvent: ToolCallResultEvent = {
894
+ type: EventType.TOOL_CALL_RESULT,
895
+ role: "tool",
896
+ messageId: randomUUID(),
897
+ toolCallId: part.toolCallId,
898
+ content: JSON.stringify(toolResult),
899
+ };
900
+ subscriber.next(resultEvent);
901
+ break;
902
+ }
903
+
904
+ case "finish":
905
+ // Emit run finished event
906
+ const finishedEvent: RunFinishedEvent = {
907
+ type: EventType.RUN_FINISHED,
908
+ threadId: input.threadId,
909
+ runId: input.runId,
910
+ };
911
+ subscriber.next(finishedEvent);
912
+ terminalEventEmitted = true;
913
+
914
+ // Complete the observable
915
+ subscriber.complete();
916
+ break;
917
+
918
+ case "error": {
919
+ if (abortController.signal.aborted) {
920
+ break;
921
+ }
922
+ const runErrorEvent: RunErrorEvent = {
923
+ type: EventType.RUN_ERROR,
924
+ message: part.error + "",
925
+ };
926
+ subscriber.next(runErrorEvent);
927
+ terminalEventEmitted = true;
928
+
929
+ // Handle error
930
+ subscriber.error(part.error);
931
+ break;
932
+ }
933
+ }
934
+ }
935
+
936
+ if (!terminalEventEmitted) {
937
+ if (abortController.signal.aborted) {
938
+ // Let the runner finalize the stream on stop requests so it can
939
+ // inject consistent closing events and a RUN_FINISHED marker.
940
+ } else {
941
+ const finishedEvent: RunFinishedEvent = {
942
+ type: EventType.RUN_FINISHED,
943
+ threadId: input.threadId,
944
+ runId: input.runId,
945
+ };
946
+ subscriber.next(finishedEvent);
947
+ }
948
+
949
+ terminalEventEmitted = true;
950
+ subscriber.complete();
951
+ }
952
+ } catch (error) {
953
+ if (abortController.signal.aborted) {
954
+ subscriber.complete();
955
+ } else {
956
+ const runErrorEvent: RunErrorEvent = {
957
+ type: EventType.RUN_ERROR,
958
+ message: error + "",
959
+ };
960
+ subscriber.next(runErrorEvent);
961
+ terminalEventEmitted = true;
962
+ subscriber.error(error);
963
+ }
964
+ } finally {
965
+ this.abortController = undefined;
966
+ await Promise.all(mcpClients.map((client) => client.close()));
967
+ }
968
+ })();
969
+
970
+ // Cleanup function
971
+ return () => {
972
+ // Cleanup MCP clients if stream is unsubscribed
973
+ Promise.all(mcpClients.map((client) => client.close())).catch(() => {
974
+ // Ignore cleanup errors
975
+ });
976
+ };
977
+ });
978
+ }
979
+
980
+ clone() {
981
+ const cloned = new BuiltInAgent(this.config);
982
+ // Copy middlewares from parent class
983
+ // @ts-expect-error - accessing protected property from parent
984
+ cloned.middlewares = [...this.middlewares];
985
+ return cloned;
986
+ }
987
+
988
+ abortRun(): void {
989
+ this.abortController?.abort();
990
+ }
991
+ }
992
+
993
+ /**
994
+ * @deprecated Use BuiltInAgent instead
995
+ */
996
+ export class BasicAgent extends BuiltInAgent {
997
+ constructor(config: BuiltInAgentConfiguration) {
998
+ super(config);
999
+ console.warn("BasicAgent is deprecated, use BuiltInAgent instead");
1000
+ }
1001
+ }
1002
+
1003
+ export type BasicAgentConfiguration = BuiltInAgentConfiguration;