@copilotkitnext/agent 0.0.13-alpha.0

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,894 @@
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
+ experimental_createMCPClient as createMCPClient,
32
+ } from "ai";
33
+ import { Observable } from "rxjs";
34
+ import { createOpenAI } from "@ai-sdk/openai";
35
+ import { createAnthropic } from "@ai-sdk/anthropic";
36
+ import { createGoogleGenerativeAI } from "@ai-sdk/google";
37
+ import { randomUUID } from "crypto";
38
+ import { z } from "zod";
39
+ import {
40
+ StreamableHTTPClientTransport,
41
+ StreamableHTTPClientTransportOptions,
42
+ } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
43
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
44
+ import { u } from "vitest/dist/chunks/reporters.d.BFLkQcL6.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 BasicAgent
66
+ */
67
+ export type BasicAgentModel =
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 BasicAgent
209
+ */
210
+ export interface ToolDefinition<TParameters extends z.ZodTypeAny = z.ZodTypeAny> {
211
+ name: string;
212
+ description: string;
213
+ parameters: TParameters;
214
+ }
215
+
216
+ /**
217
+ * Define a tool for use with BasicAgent
218
+ * @param name - The name of the tool
219
+ * @param description - Description of what the tool does
220
+ * @param parameters - Zod schema for the tool's input parameters
221
+ * @returns Tool definition
222
+ */
223
+ export function defineTool<TParameters extends z.ZodTypeAny>(config: {
224
+ name: string;
225
+ description: string;
226
+ parameters: TParameters;
227
+ }): ToolDefinition<TParameters> {
228
+ return {
229
+ name: config.name,
230
+ description: config.description,
231
+ parameters: config.parameters,
232
+ };
233
+ }
234
+
235
+ /**
236
+ * Converts AG-UI messages to Vercel AI SDK ModelMessage format
237
+ */
238
+ export function convertMessagesToVercelAISDKMessages(messages: Message[]): ModelMessage[] {
239
+ const result: ModelMessage[] = [];
240
+
241
+ for (const message of messages) {
242
+ if (message.role === "assistant") {
243
+ const parts: Array<TextPart | ToolCallPart> = message.content ? [{ type: "text", text: message.content }] : [];
244
+
245
+ for (const toolCall of message.toolCalls ?? []) {
246
+ const toolCallPart: ToolCallPart = {
247
+ type: "tool-call",
248
+ toolCallId: toolCall.id,
249
+ toolName: toolCall.function.name,
250
+ input: JSON.parse(toolCall.function.arguments),
251
+ };
252
+ parts.push(toolCallPart);
253
+ }
254
+
255
+ const assistantMsg: AssistantModelMessage = {
256
+ role: "assistant",
257
+ content: parts,
258
+ };
259
+ result.push(assistantMsg);
260
+ } else if (message.role === "user") {
261
+ const userMsg: UserModelMessage = {
262
+ role: "user",
263
+ content: message.content || "",
264
+ };
265
+ result.push(userMsg);
266
+ } else if (message.role === "tool") {
267
+ let toolName = "unknown";
268
+ // Find the tool name from the corresponding tool call
269
+ for (const msg of messages) {
270
+ if (msg.role === "assistant") {
271
+ for (const toolCall of msg.toolCalls ?? []) {
272
+ if (toolCall.id === message.toolCallId) {
273
+ toolName = toolCall.function.name;
274
+ break;
275
+ }
276
+ }
277
+ }
278
+ }
279
+
280
+ const toolResultPart: ToolResultPart = {
281
+ type: "tool-result",
282
+ toolCallId: message.toolCallId,
283
+ toolName: toolName,
284
+ output: {
285
+ type: "text",
286
+ value: message.content,
287
+ },
288
+ };
289
+
290
+ const toolMsg: ToolModelMessage = {
291
+ role: "tool",
292
+ content: [toolResultPart],
293
+ };
294
+ result.push(toolMsg);
295
+ }
296
+ }
297
+
298
+ return result;
299
+ }
300
+
301
+ /**
302
+ * JSON Schema type definition
303
+ */
304
+ interface JsonSchema {
305
+ type: "object" | "string" | "number" | "boolean" | "array";
306
+ description?: string;
307
+ properties?: Record<string, JsonSchema>;
308
+ required?: string[];
309
+ items?: JsonSchema;
310
+ }
311
+
312
+ /**
313
+ * Converts JSON Schema to Zod schema
314
+ */
315
+ export function convertJsonSchemaToZodSchema(jsonSchema: JsonSchema, required: boolean): z.ZodSchema {
316
+ if (jsonSchema.type === "object") {
317
+ const spec: { [key: string]: z.ZodSchema } = {};
318
+
319
+ if (!jsonSchema.properties || !Object.keys(jsonSchema.properties).length) {
320
+ return !required ? z.object(spec).optional() : z.object(spec);
321
+ }
322
+
323
+ for (const [key, value] of Object.entries(jsonSchema.properties)) {
324
+ spec[key] = convertJsonSchemaToZodSchema(value, jsonSchema.required ? jsonSchema.required.includes(key) : false);
325
+ }
326
+ let schema = z.object(spec).describe(jsonSchema.description ?? "");
327
+ return required ? schema : schema.optional();
328
+ } else if (jsonSchema.type === "string") {
329
+ let schema = z.string().describe(jsonSchema.description ?? "");
330
+ return required ? schema : schema.optional();
331
+ } else if (jsonSchema.type === "number") {
332
+ let schema = z.number().describe(jsonSchema.description ?? "");
333
+ return required ? schema : schema.optional();
334
+ } else if (jsonSchema.type === "boolean") {
335
+ let schema = z.boolean().describe(jsonSchema.description ?? "");
336
+ return required ? schema : schema.optional();
337
+ } else if (jsonSchema.type === "array") {
338
+ if (!jsonSchema.items) {
339
+ throw new Error("Array type must have items property");
340
+ }
341
+ let itemSchema = convertJsonSchemaToZodSchema(jsonSchema.items, true);
342
+ let schema = z.array(itemSchema).describe(jsonSchema.description ?? "");
343
+ return required ? schema : schema.optional();
344
+ }
345
+ throw new Error("Invalid JSON schema");
346
+ }
347
+
348
+ /**
349
+ * Converts AG-UI tools to Vercel AI SDK ToolSet
350
+ */
351
+ function isJsonSchema(obj: unknown): obj is JsonSchema {
352
+ if (typeof obj !== "object" || obj === null) return false;
353
+ const schema = obj as Record<string, unknown>;
354
+ return typeof schema.type === "string" && ["object", "string", "number", "boolean", "array"].includes(schema.type);
355
+ }
356
+
357
+ export function convertToolsToVercelAITools(tools: RunAgentInput["tools"]): ToolSet {
358
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
359
+ const result: Record<string, any> = {};
360
+
361
+ for (const tool of tools) {
362
+ if (!isJsonSchema(tool.parameters)) {
363
+ throw new Error(`Invalid JSON schema for tool ${tool.name}`);
364
+ }
365
+ const zodSchema = convertJsonSchemaToZodSchema(tool.parameters, true);
366
+ result[tool.name] = createVercelAISDKTool({
367
+ description: tool.description,
368
+ inputSchema: zodSchema,
369
+ });
370
+ }
371
+
372
+ return result;
373
+ }
374
+
375
+ /**
376
+ * Converts ToolDefinition array to Vercel AI SDK ToolSet
377
+ */
378
+ export function convertToolDefinitionsToVercelAITools(tools: ToolDefinition[]): ToolSet {
379
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
380
+ const result: Record<string, any> = {};
381
+
382
+ for (const tool of tools) {
383
+ result[tool.name] = createVercelAISDKTool({
384
+ description: tool.description,
385
+ inputSchema: tool.parameters,
386
+ });
387
+ }
388
+
389
+ return result;
390
+ }
391
+
392
+ /**
393
+ * Configuration for BasicAgent
394
+ */
395
+ export interface BasicAgentConfiguration {
396
+ /**
397
+ * The model to use
398
+ */
399
+ model: BasicAgentModel | LanguageModel;
400
+ /**
401
+ * Maximum number of steps/iterations for tool calling (default: 1)
402
+ */
403
+ maxSteps?: number;
404
+ /**
405
+ * Tool choice setting - how tools are selected for execution (default: "auto")
406
+ */
407
+ toolChoice?: ToolChoice<Record<string, unknown>>;
408
+ /**
409
+ * Maximum number of tokens to generate
410
+ */
411
+ maxOutputTokens?: number;
412
+ /**
413
+ * Temperature setting (range depends on provider)
414
+ */
415
+ temperature?: number;
416
+ /**
417
+ * Nucleus sampling (topP)
418
+ */
419
+ topP?: number;
420
+ /**
421
+ * Top K sampling
422
+ */
423
+ topK?: number;
424
+ /**
425
+ * Presence penalty
426
+ */
427
+ presencePenalty?: number;
428
+ /**
429
+ * Frequency penalty
430
+ */
431
+ frequencyPenalty?: number;
432
+ /**
433
+ * Sequences that will stop the generation
434
+ */
435
+ stopSequences?: string[];
436
+ /**
437
+ * Seed for deterministic results
438
+ */
439
+ seed?: number;
440
+ /**
441
+ * Maximum number of retries
442
+ */
443
+ maxRetries?: number;
444
+ /**
445
+ * Prompt for the agent
446
+ */
447
+ prompt?: string;
448
+ /**
449
+ * List of properties that can be overridden by forwardedProps.
450
+ */
451
+ overridableProperties?: OverridableProperty[];
452
+ /**
453
+ * Optional list of MCP server configurations
454
+ */
455
+ mcpServers?: MCPClientConfig[];
456
+ /**
457
+ * Optional tools available to the agent
458
+ */
459
+ tools?: ToolDefinition[];
460
+ }
461
+
462
+ export class BasicAgent extends AbstractAgent {
463
+ constructor(private config: BasicAgentConfiguration) {
464
+ super();
465
+ }
466
+
467
+ /**
468
+ * Check if a property can be overridden by forwardedProps
469
+ */
470
+ canOverride(property: OverridableProperty): boolean {
471
+ return this.config?.overridableProperties?.includes(property) ?? false;
472
+ }
473
+
474
+ protected run(input: RunAgentInput): Observable<BaseEvent> {
475
+ return new Observable<BaseEvent>((subscriber) => {
476
+ // Emit RUN_STARTED event
477
+ const startEvent: RunStartedEvent = {
478
+ type: EventType.RUN_STARTED,
479
+ threadId: input.threadId,
480
+ runId: input.runId,
481
+ };
482
+ subscriber.next(startEvent);
483
+
484
+ // Resolve the model
485
+ const model = resolveModel(this.config.model);
486
+
487
+ // Build prompt based on conditions
488
+ let systemPrompt: string | undefined = undefined;
489
+
490
+ // Check if we should build a prompt:
491
+ // - config.prompt is set, OR
492
+ // - input.context is non-empty, OR
493
+ // - input.state is non-empty and not an empty object
494
+ const hasPrompt = !!this.config.prompt;
495
+ const hasContext = input.context && input.context.length > 0;
496
+ const hasState =
497
+ input.state !== undefined &&
498
+ input.state !== null &&
499
+ !(typeof input.state === "object" && Object.keys(input.state).length === 0);
500
+
501
+ if (hasPrompt || hasContext || hasState) {
502
+ const parts: string[] = [];
503
+
504
+ // First: the prompt if any
505
+ if (hasPrompt) {
506
+ parts.push(this.config.prompt!);
507
+ }
508
+
509
+ // Second: context from the application
510
+ if (hasContext) {
511
+ parts.push("\n## Context from the application\n");
512
+ for (const ctx of input.context) {
513
+ parts.push(`${ctx.description}:\n${ctx.value}\n`);
514
+ }
515
+ }
516
+
517
+ // Third: state from the application that can be edited
518
+ if (hasState) {
519
+ parts.push(
520
+ "\n## Application State\n" +
521
+ "This is state from the application that you can edit by calling AGUISendStateSnapshot or AGUISendStateDelta.\n" +
522
+ `\`\`\`json\n${JSON.stringify(input.state, null, 2)}\n\`\`\`\n`,
523
+ );
524
+ }
525
+
526
+ systemPrompt = parts.join("");
527
+ }
528
+
529
+ // Convert messages and prepend system message if we have a prompt
530
+ const messages = convertMessagesToVercelAISDKMessages(input.messages);
531
+ if (systemPrompt) {
532
+ messages.unshift({
533
+ role: "system",
534
+ content: systemPrompt,
535
+ });
536
+ }
537
+
538
+ // Merge tools from input and config
539
+ let allTools: ToolSet = convertToolsToVercelAITools(input.tools);
540
+ if (this.config.tools && this.config.tools.length > 0) {
541
+ const configTools = convertToolDefinitionsToVercelAITools(this.config.tools);
542
+ allTools = { ...allTools, ...configTools };
543
+ }
544
+
545
+ const streamTextParams: Parameters<typeof streamText>[0] = {
546
+ model,
547
+ messages,
548
+ tools: allTools,
549
+ toolChoice: this.config.toolChoice,
550
+ maxOutputTokens: this.config.maxOutputTokens,
551
+ temperature: this.config.temperature,
552
+ topP: this.config.topP,
553
+ topK: this.config.topK,
554
+ presencePenalty: this.config.presencePenalty,
555
+ frequencyPenalty: this.config.frequencyPenalty,
556
+ stopSequences: this.config.stopSequences,
557
+ seed: this.config.seed,
558
+ maxRetries: this.config.maxRetries,
559
+ };
560
+
561
+ // Apply forwardedProps overrides (if allowed)
562
+ if (input.forwardedProps && typeof input.forwardedProps === "object") {
563
+ const props = input.forwardedProps as Record<string, unknown>;
564
+
565
+ // Check and apply each overridable property
566
+ if (props.model !== undefined && this.canOverride("model")) {
567
+ if (typeof props.model === "string" || typeof props.model === "object") {
568
+ // Accept any string or LanguageModel instance for model override
569
+ streamTextParams.model = resolveModel(props.model as string | LanguageModel);
570
+ }
571
+ }
572
+ if (props.toolChoice !== undefined && this.canOverride("toolChoice")) {
573
+ // ToolChoice can be 'auto', 'required', 'none', or { type: 'tool', toolName: string }
574
+ const toolChoice = props.toolChoice;
575
+ if (
576
+ toolChoice === "auto" ||
577
+ toolChoice === "required" ||
578
+ toolChoice === "none" ||
579
+ (typeof toolChoice === "object" &&
580
+ toolChoice !== null &&
581
+ "type" in toolChoice &&
582
+ toolChoice.type === "tool")
583
+ ) {
584
+ streamTextParams.toolChoice = toolChoice as ToolChoice<Record<string, unknown>>;
585
+ }
586
+ }
587
+ if (typeof props.maxOutputTokens === "number" && this.canOverride("maxOutputTokens")) {
588
+ streamTextParams.maxOutputTokens = props.maxOutputTokens;
589
+ }
590
+ if (typeof props.temperature === "number" && this.canOverride("temperature")) {
591
+ streamTextParams.temperature = props.temperature;
592
+ }
593
+ if (typeof props.topP === "number" && this.canOverride("topP")) {
594
+ streamTextParams.topP = props.topP;
595
+ }
596
+ if (typeof props.topK === "number" && this.canOverride("topK")) {
597
+ streamTextParams.topK = props.topK;
598
+ }
599
+ if (typeof props.presencePenalty === "number" && this.canOverride("presencePenalty")) {
600
+ streamTextParams.presencePenalty = props.presencePenalty;
601
+ }
602
+ if (typeof props.frequencyPenalty === "number" && this.canOverride("frequencyPenalty")) {
603
+ streamTextParams.frequencyPenalty = props.frequencyPenalty;
604
+ }
605
+ if (Array.isArray(props.stopSequences) && this.canOverride("stopSequences")) {
606
+ // Validate all elements are strings
607
+ if (props.stopSequences.every((item): item is string => typeof item === "string")) {
608
+ streamTextParams.stopSequences = props.stopSequences;
609
+ }
610
+ }
611
+ if (typeof props.seed === "number" && this.canOverride("seed")) {
612
+ streamTextParams.seed = props.seed;
613
+ }
614
+ if (typeof props.maxRetries === "number" && this.canOverride("maxRetries")) {
615
+ streamTextParams.maxRetries = props.maxRetries;
616
+ }
617
+ }
618
+
619
+ // Set up MCP clients if configured and process the stream
620
+ const mcpClients: Array<{ close: () => Promise<void> }> = [];
621
+
622
+ (async () => {
623
+ try {
624
+ // Add AG-UI state update tools
625
+ streamTextParams.tools = {
626
+ ...streamTextParams.tools,
627
+ AGUISendStateSnapshot: createVercelAISDKTool({
628
+ description: "Replace the entire application state with a new snapshot",
629
+ inputSchema: z.object({
630
+ snapshot: z.any().describe("The complete new state object"),
631
+ }),
632
+ execute: async ({ snapshot }) => {
633
+ return { success: true, snapshot };
634
+ },
635
+ }),
636
+ AGUISendStateDelta: createVercelAISDKTool({
637
+ description: "Apply incremental updates to application state using JSON Patch operations",
638
+ inputSchema: z.object({
639
+ delta: z
640
+ .array(
641
+ z.object({
642
+ op: z.enum(["add", "replace", "remove"]).describe("The operation to perform"),
643
+ path: z.string().describe("JSON Pointer path (e.g., '/foo/bar')"),
644
+ value: z
645
+ .any()
646
+ .optional()
647
+ .describe(
648
+ "The value to set. Required for 'add' and 'replace' operations, ignored for 'remove'.",
649
+ ),
650
+ }),
651
+ )
652
+ .describe("Array of JSON Patch operations"),
653
+ }),
654
+ execute: async ({ delta }) => {
655
+ return { success: true, delta };
656
+ },
657
+ }),
658
+ };
659
+
660
+ // Initialize MCP clients and get their tools
661
+ if (this.config.mcpServers && this.config.mcpServers.length > 0) {
662
+ for (const serverConfig of this.config.mcpServers) {
663
+ let transport;
664
+
665
+ if (serverConfig.type === "http") {
666
+ const url = new URL(serverConfig.url);
667
+ transport = new StreamableHTTPClientTransport(url, serverConfig.options);
668
+ } else if (serverConfig.type === "sse") {
669
+ transport = new SSEClientTransport(new URL(serverConfig.url), serverConfig.headers);
670
+ }
671
+
672
+ if (transport) {
673
+ const mcpClient = await createMCPClient({ transport });
674
+ mcpClients.push(mcpClient);
675
+
676
+ // Get tools from this MCP server and merge with existing tools
677
+ const mcpTools = await mcpClient.tools();
678
+ streamTextParams.tools = { ...streamTextParams.tools, ...mcpTools };
679
+ }
680
+ }
681
+ }
682
+
683
+ // Call streamText and process the stream
684
+ const response = streamText(streamTextParams);
685
+
686
+ let messageId = randomUUID();
687
+
688
+ const toolCallStates = new Map<
689
+ string,
690
+ {
691
+ started: boolean;
692
+ hasArgsDelta: boolean;
693
+ ended: boolean;
694
+ toolName?: string;
695
+ }
696
+ >();
697
+
698
+ const ensureToolCallState = (toolCallId: string) => {
699
+ let state = toolCallStates.get(toolCallId);
700
+ if (!state) {
701
+ state = { started: false, hasArgsDelta: false, ended: false };
702
+ toolCallStates.set(toolCallId, state);
703
+ }
704
+ return state;
705
+ };
706
+
707
+ // Process fullStream events
708
+ for await (const part of response.fullStream) {
709
+ switch (part.type) {
710
+ case "tool-input-start": {
711
+ const toolCallId = part.id;
712
+ const state = ensureToolCallState(toolCallId);
713
+ state.toolName = part.toolName;
714
+ if (!state.started) {
715
+ state.started = true;
716
+ const startEvent: ToolCallStartEvent = {
717
+ type: EventType.TOOL_CALL_START,
718
+ parentMessageId: messageId,
719
+ toolCallId,
720
+ toolCallName: part.toolName,
721
+ };
722
+ subscriber.next(startEvent);
723
+ }
724
+ break;
725
+ }
726
+
727
+ case "tool-input-delta": {
728
+ const toolCallId = part.id;
729
+ const state = ensureToolCallState(toolCallId);
730
+ state.hasArgsDelta = true;
731
+ const argsEvent: ToolCallArgsEvent = {
732
+ type: EventType.TOOL_CALL_ARGS,
733
+ toolCallId,
734
+ delta: part.delta,
735
+ };
736
+ subscriber.next(argsEvent);
737
+ break;
738
+ }
739
+
740
+ case "tool-input-end": {
741
+ // No direct event – the subsequent "tool-call" part marks completion.
742
+ break;
743
+ }
744
+
745
+ case "text-delta": {
746
+ // Accumulate text content - in AI SDK 5.0, the property is 'text'
747
+ const textDelta = "text" in part ? part.text : "";
748
+ // Emit text chunk event
749
+ const textEvent: TextMessageChunkEvent = {
750
+ type: EventType.TEXT_MESSAGE_CHUNK,
751
+ role: "assistant",
752
+ messageId,
753
+ delta: textDelta,
754
+ };
755
+ subscriber.next(textEvent);
756
+ break;
757
+ }
758
+
759
+ case "tool-call": {
760
+ const toolCallId = part.toolCallId;
761
+ const state = ensureToolCallState(toolCallId);
762
+ state.toolName = part.toolName ?? state.toolName;
763
+
764
+ if (!state.started) {
765
+ state.started = true;
766
+ const startEvent: ToolCallStartEvent = {
767
+ type: EventType.TOOL_CALL_START,
768
+ parentMessageId: messageId,
769
+ toolCallId,
770
+ toolCallName: part.toolName,
771
+ };
772
+ subscriber.next(startEvent);
773
+ }
774
+
775
+ if (!state.hasArgsDelta && "input" in part && part.input !== undefined) {
776
+ let serializedInput = "";
777
+ if (typeof part.input === "string") {
778
+ serializedInput = part.input;
779
+ } else {
780
+ try {
781
+ serializedInput = JSON.stringify(part.input);
782
+ } catch {
783
+ serializedInput = String(part.input);
784
+ }
785
+ }
786
+
787
+ if (serializedInput.length > 0) {
788
+ const argsEvent: ToolCallArgsEvent = {
789
+ type: EventType.TOOL_CALL_ARGS,
790
+ toolCallId,
791
+ delta: serializedInput,
792
+ };
793
+ subscriber.next(argsEvent);
794
+ state.hasArgsDelta = true;
795
+ }
796
+ }
797
+
798
+ if (!state.ended) {
799
+ state.ended = true;
800
+ const endEvent: ToolCallEndEvent = {
801
+ type: EventType.TOOL_CALL_END,
802
+ toolCallId,
803
+ };
804
+ subscriber.next(endEvent);
805
+ }
806
+ break;
807
+ }
808
+
809
+ case "tool-result": {
810
+ const toolResult = "output" in part ? part.output : null;
811
+ const toolName = "toolName" in part ? part.toolName : "";
812
+ toolCallStates.delete(part.toolCallId);
813
+
814
+ // Check if this is a state update tool
815
+ if (toolName === "AGUISendStateSnapshot" && toolResult && typeof toolResult === "object") {
816
+ // Emit StateSnapshotEvent
817
+ const stateSnapshotEvent: StateSnapshotEvent = {
818
+ type: EventType.STATE_SNAPSHOT,
819
+ snapshot: toolResult.snapshot,
820
+ };
821
+ subscriber.next(stateSnapshotEvent);
822
+ } else if (toolName === "AGUISendStateDelta" && toolResult && typeof toolResult === "object") {
823
+ // Emit StateDeltaEvent
824
+ const stateDeltaEvent: StateDeltaEvent = {
825
+ type: EventType.STATE_DELTA,
826
+ delta: toolResult.delta,
827
+ };
828
+ subscriber.next(stateDeltaEvent);
829
+ }
830
+
831
+ // Always emit the tool result event for the LLM
832
+ const resultEvent: ToolCallResultEvent = {
833
+ type: EventType.TOOL_CALL_RESULT,
834
+ role: "tool",
835
+ messageId: randomUUID(),
836
+ toolCallId: part.toolCallId,
837
+ content: JSON.stringify(toolResult),
838
+ };
839
+ subscriber.next(resultEvent);
840
+ break;
841
+ }
842
+
843
+ case "finish":
844
+ // Emit run finished event
845
+ const finishedEvent: RunFinishedEvent = {
846
+ type: EventType.RUN_FINISHED,
847
+ threadId: input.threadId,
848
+ runId: input.runId,
849
+ };
850
+ subscriber.next(finishedEvent);
851
+
852
+ // Complete the observable
853
+ subscriber.complete();
854
+ break;
855
+
856
+ case "error":
857
+ const runErrorEvent: RunErrorEvent = {
858
+ type: EventType.RUN_ERROR,
859
+ message: part.error + "",
860
+ };
861
+ subscriber.next(runErrorEvent);
862
+
863
+ // Handle error
864
+ subscriber.error(part.error);
865
+ break;
866
+ }
867
+ }
868
+ } catch (error) {
869
+ const runErrorEvent: RunErrorEvent = {
870
+ type: EventType.RUN_ERROR,
871
+ message: error + "",
872
+ };
873
+ subscriber.next(runErrorEvent);
874
+
875
+ subscriber.error(error);
876
+ } finally {
877
+ await Promise.all(mcpClients.map((client) => client.close()));
878
+ }
879
+ })();
880
+
881
+ // Cleanup function
882
+ return () => {
883
+ // Cleanup MCP clients if stream is unsubscribed
884
+ Promise.all(mcpClients.map((client) => client.close())).catch(() => {
885
+ // Ignore cleanup errors
886
+ });
887
+ };
888
+ });
889
+ }
890
+
891
+ clone() {
892
+ return new BasicAgent(this.config);
893
+ }
894
+ }