@inkeep/agents-run-api 0.1.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.
Files changed (149) hide show
  1. package/README.md +117 -0
  2. package/dist/AgentExecutionServer.d.ts +23 -0
  3. package/dist/AgentExecutionServer.d.ts.map +1 -0
  4. package/dist/AgentExecutionServer.js +32 -0
  5. package/dist/__tests__/setup.d.ts +4 -0
  6. package/dist/__tests__/setup.d.ts.map +1 -0
  7. package/dist/__tests__/setup.js +50 -0
  8. package/dist/__tests__/utils/testProject.d.ts +18 -0
  9. package/dist/__tests__/utils/testProject.d.ts.map +1 -0
  10. package/dist/__tests__/utils/testProject.js +26 -0
  11. package/dist/__tests__/utils/testRequest.d.ts +8 -0
  12. package/dist/__tests__/utils/testRequest.d.ts.map +1 -0
  13. package/dist/__tests__/utils/testRequest.js +32 -0
  14. package/dist/__tests__/utils/testTenant.d.ts +64 -0
  15. package/dist/__tests__/utils/testTenant.d.ts.map +1 -0
  16. package/dist/__tests__/utils/testTenant.js +71 -0
  17. package/dist/a2a/client.d.ts +182 -0
  18. package/dist/a2a/client.d.ts.map +1 -0
  19. package/dist/a2a/client.js +645 -0
  20. package/dist/a2a/handlers.d.ts +4 -0
  21. package/dist/a2a/handlers.d.ts.map +1 -0
  22. package/dist/a2a/handlers.js +657 -0
  23. package/dist/a2a/transfer.d.ts +18 -0
  24. package/dist/a2a/transfer.d.ts.map +1 -0
  25. package/dist/a2a/transfer.js +22 -0
  26. package/dist/a2a/types.d.ts +63 -0
  27. package/dist/a2a/types.d.ts.map +1 -0
  28. package/dist/a2a/types.js +1 -0
  29. package/dist/agents/Agent.d.ts +154 -0
  30. package/dist/agents/Agent.d.ts.map +1 -0
  31. package/dist/agents/Agent.js +1105 -0
  32. package/dist/agents/ModelFactory.d.ts +62 -0
  33. package/dist/agents/ModelFactory.d.ts.map +1 -0
  34. package/dist/agents/ModelFactory.js +208 -0
  35. package/dist/agents/SystemPromptBuilder.d.ts +14 -0
  36. package/dist/agents/SystemPromptBuilder.d.ts.map +1 -0
  37. package/dist/agents/SystemPromptBuilder.js +62 -0
  38. package/dist/agents/ToolSessionManager.d.ts +61 -0
  39. package/dist/agents/ToolSessionManager.d.ts.map +1 -0
  40. package/dist/agents/ToolSessionManager.js +143 -0
  41. package/dist/agents/artifactTools.d.ts +30 -0
  42. package/dist/agents/artifactTools.d.ts.map +1 -0
  43. package/dist/agents/artifactTools.js +463 -0
  44. package/dist/agents/generateTaskHandler.d.ts +41 -0
  45. package/dist/agents/generateTaskHandler.d.ts.map +1 -0
  46. package/dist/agents/generateTaskHandler.js +350 -0
  47. package/dist/agents/relationTools.d.ts +33 -0
  48. package/dist/agents/relationTools.d.ts.map +1 -0
  49. package/dist/agents/relationTools.js +245 -0
  50. package/dist/agents/types.d.ts +23 -0
  51. package/dist/agents/types.d.ts.map +1 -0
  52. package/dist/agents/types.js +1 -0
  53. package/dist/agents/versions/V1Config.d.ts +21 -0
  54. package/dist/agents/versions/V1Config.d.ts.map +1 -0
  55. package/dist/agents/versions/V1Config.js +285 -0
  56. package/dist/app.d.ts +4 -0
  57. package/dist/app.d.ts.map +1 -0
  58. package/dist/app.js +194 -0
  59. package/dist/data/agentGraph.d.ts +4 -0
  60. package/dist/data/agentGraph.d.ts.map +1 -0
  61. package/dist/data/agentGraph.js +73 -0
  62. package/dist/data/agents.d.ts +4 -0
  63. package/dist/data/agents.d.ts.map +1 -0
  64. package/dist/data/agents.js +73 -0
  65. package/dist/data/conversations.d.ts +59 -0
  66. package/dist/data/conversations.d.ts.map +1 -0
  67. package/dist/data/conversations.js +216 -0
  68. package/dist/data/db/clean.d.ts +6 -0
  69. package/dist/data/db/clean.d.ts.map +1 -0
  70. package/dist/data/db/clean.js +77 -0
  71. package/dist/data/db/dbClient.d.ts +3 -0
  72. package/dist/data/db/dbClient.d.ts.map +1 -0
  73. package/dist/data/db/dbClient.js +13 -0
  74. package/dist/env.d.ts +43 -0
  75. package/dist/env.d.ts.map +1 -0
  76. package/dist/env.js +63 -0
  77. package/dist/handlers/executionHandler.d.ts +36 -0
  78. package/dist/handlers/executionHandler.d.ts.map +1 -0
  79. package/dist/handlers/executionHandler.js +402 -0
  80. package/dist/index.d.ts +5 -0
  81. package/dist/index.d.ts.map +1 -0
  82. package/dist/index.js +43 -0
  83. package/dist/instrumentation.d.ts +13 -0
  84. package/dist/instrumentation.d.ts.map +1 -0
  85. package/dist/instrumentation.js +66 -0
  86. package/dist/logger.d.ts +4 -0
  87. package/dist/logger.d.ts.map +1 -0
  88. package/dist/logger.js +32 -0
  89. package/dist/middleware/api-key-auth.d.ts +22 -0
  90. package/dist/middleware/api-key-auth.d.ts.map +1 -0
  91. package/dist/middleware/api-key-auth.js +139 -0
  92. package/dist/middleware/index.d.ts +2 -0
  93. package/dist/middleware/index.d.ts.map +1 -0
  94. package/dist/middleware/index.js +1 -0
  95. package/dist/openapi.d.ts +2 -0
  96. package/dist/openapi.d.ts.map +1 -0
  97. package/dist/openapi.js +36 -0
  98. package/dist/routes/agents.d.ts +4 -0
  99. package/dist/routes/agents.d.ts.map +1 -0
  100. package/dist/routes/agents.js +155 -0
  101. package/dist/routes/chat.d.ts +4 -0
  102. package/dist/routes/chat.d.ts.map +1 -0
  103. package/dist/routes/chat.js +308 -0
  104. package/dist/routes/chatDataStream.d.ts +4 -0
  105. package/dist/routes/chatDataStream.d.ts.map +1 -0
  106. package/dist/routes/chatDataStream.js +179 -0
  107. package/dist/routes/mcp.d.ts +4 -0
  108. package/dist/routes/mcp.d.ts.map +1 -0
  109. package/dist/routes/mcp.js +500 -0
  110. package/dist/tracer.d.ts +24 -0
  111. package/dist/tracer.d.ts.map +1 -0
  112. package/dist/tracer.js +97 -0
  113. package/dist/types/chat.d.ts +25 -0
  114. package/dist/types/chat.d.ts.map +1 -0
  115. package/dist/types/chat.js +1 -0
  116. package/dist/types/execution-context.d.ts +14 -0
  117. package/dist/types/execution-context.d.ts.map +1 -0
  118. package/dist/types/execution-context.js +14 -0
  119. package/dist/utils/agent-operations.d.ts +79 -0
  120. package/dist/utils/agent-operations.d.ts.map +1 -0
  121. package/dist/utils/agent-operations.js +67 -0
  122. package/dist/utils/artifact-component-schema.d.ts +29 -0
  123. package/dist/utils/artifact-component-schema.d.ts.map +1 -0
  124. package/dist/utils/artifact-component-schema.js +119 -0
  125. package/dist/utils/artifact-parser.d.ts +71 -0
  126. package/dist/utils/artifact-parser.d.ts.map +1 -0
  127. package/dist/utils/artifact-parser.js +251 -0
  128. package/dist/utils/cleanup.d.ts +19 -0
  129. package/dist/utils/cleanup.d.ts.map +1 -0
  130. package/dist/utils/cleanup.js +66 -0
  131. package/dist/utils/data-component-schema.d.ts +6 -0
  132. package/dist/utils/data-component-schema.d.ts.map +1 -0
  133. package/dist/utils/data-component-schema.js +43 -0
  134. package/dist/utils/graph-session.d.ts +200 -0
  135. package/dist/utils/graph-session.d.ts.map +1 -0
  136. package/dist/utils/graph-session.js +1009 -0
  137. package/dist/utils/incremental-stream-parser.d.ts +57 -0
  138. package/dist/utils/incremental-stream-parser.d.ts.map +1 -0
  139. package/dist/utils/incremental-stream-parser.js +287 -0
  140. package/dist/utils/response-formatter.d.ts +27 -0
  141. package/dist/utils/response-formatter.d.ts.map +1 -0
  142. package/dist/utils/response-formatter.js +160 -0
  143. package/dist/utils/stream-helpers.d.ts +162 -0
  144. package/dist/utils/stream-helpers.d.ts.map +1 -0
  145. package/dist/utils/stream-helpers.js +385 -0
  146. package/dist/utils/stream-registry.d.ts +18 -0
  147. package/dist/utils/stream-registry.d.ts.map +1 -0
  148. package/dist/utils/stream-registry.js +33 -0
  149. package/package.json +88 -0
@@ -0,0 +1,1105 @@
1
+ import { SpanStatusCode, trace } from '@opentelemetry/api';
2
+ import { generateObject, generateText, streamText, tool } from 'ai';
3
+ import { z } from 'zod';
4
+ import { getContextConfigById, getLedgerArtifacts, getCredentialReference, listTaskIdsByContextId, getFullGraphDefinition, graphHasArtifactComponents, ContextResolver, TemplateEngine, McpClient, CredentialStuffer, } from '@inkeep/agents-core';
5
+ import { createDefaultConversationHistoryConfig, getFormattedConversationHistory, } from '../data/conversations.js';
6
+ import dbClient from '../data/db/dbClient.js';
7
+ import { executionServer } from '../index.js';
8
+ import { getLogger } from '../logger.js';
9
+ import { createSpanName, getGlobalTracer, handleSpanError, forceFlushTracer } from '../tracer.js';
10
+ import { generateToolId } from '../utils/agent-operations.js';
11
+ import { ArtifactReferenceSchema } from '../utils/artifact-component-schema.js';
12
+ import { jsonSchemaToZod } from '../utils/data-component-schema.js';
13
+ import { graphSessionManager } from '../utils/graph-session.js';
14
+ import { IncrementalStreamParser } from '../utils/incremental-stream-parser.js';
15
+ import { ResponseFormatter } from '../utils/response-formatter.js';
16
+ import { getStreamHelper } from '../utils/stream-registry.js';
17
+ import { createSaveToolResultTool } from './artifactTools.js';
18
+ import { ModelFactory } from './ModelFactory.js';
19
+ import { createDelegateToAgentTool, createTransferToAgentTool } from './relationTools.js';
20
+ import { SystemPromptBuilder } from './SystemPromptBuilder.js';
21
+ import { toolSessionManager } from './ToolSessionManager.js';
22
+ import { V1Config } from './versions/V1Config.js';
23
+ /**
24
+ * Creates a stopWhen condition that stops when any tool call name starts with the given prefix
25
+ * @param prefix - The prefix to check for in tool call names
26
+ * @returns A function that can be used as a stopWhen condition
27
+ */
28
+ export function hasToolCallWithPrefix(prefix) {
29
+ return ({ steps }) => {
30
+ const last = steps.at(-1);
31
+ if (last && 'toolCalls' in last && last.toolCalls) {
32
+ return last.toolCalls.some((tc) => tc.toolName.startsWith(prefix));
33
+ }
34
+ return false;
35
+ };
36
+ }
37
+ const logger = getLogger('Agent');
38
+ // Get tracer using centralized utility
39
+ const tracer = getGlobalTracer();
40
+ // Constants for agent configuration
41
+ const CONSTANTS = {
42
+ MAX_GENERATION_STEPS: 12,
43
+ DEFAULT_PRIMARY_MODEL: 'anthropic/claude-4-sonnet-20250514',
44
+ DEFAULT_STRUCTURED_OUTPUT_MODEL: 'openai/gpt-4.1-mini-2025-04-14',
45
+ DEFAULT_SUMMARIZER_MODEL: 'openai/gpt-4.1-nano-2025-04-14',
46
+ PHASE_1_TIMEOUT_MS: 270_000, // 4.5 minutes for streaming phase 1
47
+ NON_STREAMING_PHASE_1_TIMEOUT_MS: 90_000, // 1.5 minutes for non-streaming phase 1
48
+ PHASE_2_TIMEOUT_MS: 90_000, // 1.5 minutes for phase 2 structured output
49
+ };
50
+ // Helper function to handle empty model strings
51
+ function getModelOrDefault(modelString, defaultModel) {
52
+ return modelString?.trim() || defaultModel;
53
+ }
54
+ // LLM Generated Information as a config LLM? Separate Step?
55
+ export class Agent {
56
+ config;
57
+ systemPromptBuilder = new SystemPromptBuilder('v1', new V1Config());
58
+ responseFormatter;
59
+ credentialStuffer;
60
+ streamHelper;
61
+ conversationId;
62
+ artifactComponents = [];
63
+ isDelegatedAgent = false;
64
+ contextResolver;
65
+ constructor(config, framework) {
66
+ // Store artifact components separately
67
+ this.artifactComponents = config.artifactComponents || [];
68
+ // Process dataComponents (now only component-type)
69
+ let processedDataComponents = config.dataComponents || [];
70
+ // If we have artifact components, add the default artifact data component for response hydration
71
+ if (this.artifactComponents.length > 0 &&
72
+ config.dataComponents &&
73
+ config.dataComponents.length > 0) {
74
+ processedDataComponents = [
75
+ ArtifactReferenceSchema.getDataComponent(config.tenantId, config.projectId),
76
+ ...processedDataComponents,
77
+ ];
78
+ }
79
+ this.config = {
80
+ ...config,
81
+ dataComponents: processedDataComponents,
82
+ // Set default conversation history if not provided
83
+ conversationHistoryConfig: config.conversationHistoryConfig || createDefaultConversationHistoryConfig(),
84
+ };
85
+ this.responseFormatter = new ResponseFormatter(config.tenantId);
86
+ // Use provided framework, fallback to global instance if available
87
+ const effectiveFramework = framework || executionServer;
88
+ if (effectiveFramework) {
89
+ this.contextResolver = new ContextResolver(config.tenantId, config.projectId, dbClient, effectiveFramework);
90
+ this.credentialStuffer = new CredentialStuffer(effectiveFramework, this.contextResolver);
91
+ }
92
+ }
93
+ /**
94
+ * Get the maximum number of generation steps for this agent
95
+ * Uses agent's stopWhen.stepCountIs config or defaults to CONSTANTS.MAX_GENERATION_STEPS
96
+ */
97
+ getMaxGenerationSteps() {
98
+ return this.config.stopWhen?.stepCountIs ?? CONSTANTS.MAX_GENERATION_STEPS;
99
+ }
100
+ /**
101
+ * Get the primary model settings for text generation and thinking
102
+ * Defaults to claude-4-sonnet if not specified
103
+ */
104
+ getPrimaryModel() {
105
+ if (!this.config.models?.base) {
106
+ return { model: CONSTANTS.DEFAULT_PRIMARY_MODEL };
107
+ }
108
+ return {
109
+ model: getModelOrDefault(this.config.models.base.model, CONSTANTS.DEFAULT_PRIMARY_MODEL),
110
+ providerOptions: this.config.models.base.providerOptions,
111
+ };
112
+ }
113
+ /**
114
+ * Get the model settings for structured output generation
115
+ * Defaults to GPT-4.1-mini for structured outputs if not specified
116
+ */
117
+ getStructuredOutputModel() {
118
+ if (!this.config.models) {
119
+ return { model: CONSTANTS.DEFAULT_STRUCTURED_OUTPUT_MODEL };
120
+ }
121
+ // Use structured output config if available, otherwise fall back to base
122
+ const structuredConfig = this.config.models.structuredOutput;
123
+ const baseConfig = this.config.models.base;
124
+ // If structured output is explicitly configured, use only its config
125
+ if (structuredConfig) {
126
+ return {
127
+ model: getModelOrDefault(structuredConfig.model, CONSTANTS.DEFAULT_STRUCTURED_OUTPUT_MODEL),
128
+ providerOptions: structuredConfig.providerOptions,
129
+ };
130
+ }
131
+ // Fall back to base model settings if structured output not configured
132
+ return {
133
+ model: getModelOrDefault(baseConfig?.model, CONSTANTS.DEFAULT_STRUCTURED_OUTPUT_MODEL),
134
+ providerOptions: baseConfig?.providerOptions,
135
+ };
136
+ }
137
+ /**
138
+ * Get the model settings for summarization tasks
139
+ * Defaults to gpt-4.1-nano if not specified
140
+ */
141
+ getSummarizerModel() {
142
+ if (!this.config.models) {
143
+ return { model: CONSTANTS.DEFAULT_SUMMARIZER_MODEL };
144
+ }
145
+ // Use summarizer config if available, otherwise fall back to base
146
+ const summarizerConfig = this.config.models.summarizer;
147
+ const baseConfig = this.config.models.base;
148
+ // If summarizer is explicitly configured, use only its config
149
+ if (summarizerConfig) {
150
+ return {
151
+ model: getModelOrDefault(summarizerConfig.model, CONSTANTS.DEFAULT_SUMMARIZER_MODEL),
152
+ providerOptions: summarizerConfig.providerOptions,
153
+ };
154
+ }
155
+ // Fall back to base model settings if summarizer not configured
156
+ return {
157
+ model: getModelOrDefault(baseConfig?.model, CONSTANTS.DEFAULT_SUMMARIZER_MODEL),
158
+ providerOptions: baseConfig?.providerOptions,
159
+ };
160
+ }
161
+ setConversationId(conversationId) {
162
+ this.conversationId = conversationId;
163
+ }
164
+ /**
165
+ * Set delegation status for this agent instance
166
+ */
167
+ setDelegationStatus(isDelegated) {
168
+ this.isDelegatedAgent = isDelegated;
169
+ }
170
+ /**
171
+ * Get streaming helper if this agent should stream to user
172
+ * Returns undefined for delegated agents to prevent streaming data operations to user
173
+ */
174
+ getStreamingHelper() {
175
+ return this.isDelegatedAgent ? undefined : this.streamHelper;
176
+ }
177
+ /**
178
+ * Wraps a tool with streaming lifecycle tracking (start, complete, error) and GraphSession recording
179
+ */
180
+ wrapToolWithStreaming(toolName, toolDefinition, streamRequestId, toolType) {
181
+ if (!toolDefinition || typeof toolDefinition !== 'object' || !('execute' in toolDefinition)) {
182
+ return toolDefinition;
183
+ }
184
+ const originalExecute = toolDefinition.execute;
185
+ return {
186
+ ...toolDefinition,
187
+ execute: async (args, context) => {
188
+ const startTime = Date.now();
189
+ // Use the AI SDK's toolCallId consistently instead of generating our own
190
+ const toolId = context?.toolCallId || generateToolId();
191
+ const activeSpan = trace.getActiveSpan();
192
+ if (activeSpan) {
193
+ activeSpan.setAttributes({
194
+ 'conversation.id': this.conversationId,
195
+ 'tool.purpose': toolDefinition.description || 'No description provided',
196
+ 'ai.toolType': toolType || 'unknown',
197
+ 'ai.agentName': this.config.name || 'unknown',
198
+ 'graph.id': this.config.graphId || 'unknown',
199
+ });
200
+ }
201
+ // Check if this is an internal tool to skip from recording
202
+ const isInternalTool = toolName.includes('save_tool_result') ||
203
+ toolName.includes('thinking_complete') ||
204
+ toolName.startsWith('transfer_to_') ||
205
+ toolName.startsWith('delegate_to_');
206
+ try {
207
+ const result = await originalExecute(args, context);
208
+ const duration = Date.now() - startTime;
209
+ // Record complete tool execution in GraphSession (skip internal tools)
210
+ if (streamRequestId && !isInternalTool) {
211
+ graphSessionManager.recordEvent(streamRequestId, 'tool_execution', this.config.id, {
212
+ toolName,
213
+ args,
214
+ result,
215
+ toolId,
216
+ duration,
217
+ });
218
+ }
219
+ return result;
220
+ }
221
+ catch (error) {
222
+ const duration = Date.now() - startTime;
223
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
224
+ // Record tool execution with error (skip internal tools)
225
+ if (streamRequestId && !isInternalTool) {
226
+ graphSessionManager.recordEvent(streamRequestId, 'tool_execution', this.config.id, {
227
+ toolName,
228
+ args,
229
+ result: { error: errorMessage },
230
+ toolId,
231
+ duration,
232
+ });
233
+ }
234
+ throw error;
235
+ }
236
+ },
237
+ };
238
+ }
239
+ getRelationTools(runtimeContext, sessionId) {
240
+ const { transferRelations = [], delegateRelations = [] } = this.config;
241
+ const createToolName = (prefix, agentId) => `${prefix}_to_${agentId.toLowerCase().replace(/\s+/g, '_')}`;
242
+ return Object.fromEntries([
243
+ ...transferRelations.map((agentConfig) => {
244
+ const toolName = createToolName('transfer', agentConfig.id);
245
+ return [
246
+ toolName,
247
+ this.wrapToolWithStreaming(toolName, createTransferToAgentTool({
248
+ transferConfig: agentConfig,
249
+ callingAgentId: this.config.id,
250
+ agent: this,
251
+ streamRequestId: runtimeContext?.metadata?.streamRequestId,
252
+ }), runtimeContext?.metadata?.streamRequestId, 'transfer'),
253
+ ];
254
+ }),
255
+ ...delegateRelations.map((relation) => {
256
+ const toolName = createToolName('delegate', relation.config.id);
257
+ return [
258
+ toolName,
259
+ this.wrapToolWithStreaming(toolName, createDelegateToAgentTool({
260
+ delegateConfig: relation,
261
+ callingAgentId: this.config.id,
262
+ tenantId: this.config.tenantId,
263
+ projectId: this.config.projectId,
264
+ graphId: this.config.graphId,
265
+ contextId: runtimeContext?.contextId || 'default', // fallback for compatibility
266
+ metadata: runtimeContext?.metadata || {
267
+ conversationId: runtimeContext?.contextId || 'default',
268
+ threadId: runtimeContext?.contextId || 'default',
269
+ streamRequestId: runtimeContext?.metadata?.streamRequestId,
270
+ apiKey: runtimeContext?.metadata?.apiKey,
271
+ },
272
+ sessionId,
273
+ agent: this,
274
+ }), runtimeContext?.metadata?.streamRequestId, 'delegation'),
275
+ ];
276
+ }),
277
+ ]);
278
+ }
279
+ async getMcpTools(sessionId, streamRequestId) {
280
+ const tools = (await Promise.all(this.config.tools?.map((tool) => this.getMcpTool(tool)) || [])) || [];
281
+ // If no sessionId, return tools as-is (for system prompt building)
282
+ if (!sessionId) {
283
+ const combinedTools = tools.reduce((acc, tool) => {
284
+ return Object.assign(acc, tool);
285
+ }, {});
286
+ // Just wrap with streaming capability
287
+ const wrappedTools = {};
288
+ for (const [toolName, toolDef] of Object.entries(combinedTools)) {
289
+ wrappedTools[toolName] = this.wrapToolWithStreaming(toolName, toolDef, streamRequestId, 'mcp');
290
+ }
291
+ return wrappedTools;
292
+ }
293
+ // Wrap each MCP tool to record results immediately upon execution
294
+ const wrappedTools = {};
295
+ for (const toolSet of tools) {
296
+ for (const [toolName, originalTool] of Object.entries(toolSet)) {
297
+ // First wrap with session management
298
+ const sessionWrappedTool = tool({
299
+ description: originalTool.description,
300
+ inputSchema: originalTool.inputSchema,
301
+ execute: async (args, { toolCallId }) => {
302
+ logger.debug({ toolName, toolCallId }, 'MCP Tool Called');
303
+ // Call the original MCP tool
304
+ const result = await originalTool.execute(args);
305
+ // Record the result immediately in the session manager
306
+ toolSessionManager.recordToolResult(sessionId, {
307
+ toolCallId,
308
+ toolName,
309
+ args,
310
+ result,
311
+ timestamp: Date.now(),
312
+ });
313
+ return { result, toolCallId };
314
+ },
315
+ });
316
+ // Then wrap with streaming capability
317
+ wrappedTools[toolName] = this.wrapToolWithStreaming(toolName, sessionWrappedTool, streamRequestId, 'mcp');
318
+ }
319
+ }
320
+ return wrappedTools;
321
+ }
322
+ /**
323
+ * Convert database McpTool to builder MCPToolConfig format
324
+ */
325
+ convertToMCPToolConfig(tool) {
326
+ return {
327
+ id: tool.id,
328
+ name: tool.name,
329
+ description: tool.name, // Use name as description fallback
330
+ serverUrl: tool.config.mcp.server.url,
331
+ activeTools: tool.config.mcp.activeTools,
332
+ mcpType: tool.config.mcp.server.url.includes('api.nango.dev') ? 'nango' : 'generic',
333
+ transport: tool.config.mcp.transport,
334
+ headers: tool.headers,
335
+ };
336
+ }
337
+ async getMcpTool(tool) {
338
+ const credentialReferenceId = tool.credentialReferenceId;
339
+ // Build server config with credentials using new architecture
340
+ let serverConfig;
341
+ if (credentialReferenceId && this.credentialStuffer) {
342
+ // Database lookup to get credential store configuration
343
+ const credentialReference = await getCredentialReference(dbClient)({
344
+ scopes: {
345
+ tenantId: this.config.tenantId,
346
+ projectId: this.config.projectId,
347
+ },
348
+ id: credentialReferenceId,
349
+ });
350
+ if (!credentialReference) {
351
+ throw new Error(`Credential store not found: ${credentialReferenceId}`);
352
+ }
353
+ const storeReference = {
354
+ credentialStoreId: credentialReference.credentialStoreId,
355
+ retrievalParams: credentialReference.retrievalParams || {},
356
+ };
357
+ serverConfig = await this.credentialStuffer.buildMcpServerConfig({
358
+ tenantId: this.config.tenantId,
359
+ projectId: this.config.projectId,
360
+ contextConfigId: this.config.contextConfigId || undefined,
361
+ conversationId: this.conversationId || undefined,
362
+ }, this.convertToMCPToolConfig(tool), storeReference);
363
+ }
364
+ else if (tool.headers && this.credentialStuffer) {
365
+ serverConfig = await this.credentialStuffer.buildMcpServerConfig({
366
+ tenantId: this.config.tenantId,
367
+ projectId: this.config.projectId,
368
+ contextConfigId: this.config.contextConfigId || undefined,
369
+ conversationId: this.conversationId || undefined,
370
+ }, this.convertToMCPToolConfig(tool));
371
+ }
372
+ else {
373
+ // No credentials - build basic config
374
+ serverConfig = {
375
+ type: tool.config.mcp.transport?.type || 'streamable_http',
376
+ url: tool.config.mcp.server.url,
377
+ activeTools: tool.config.mcp.activeTools,
378
+ };
379
+ }
380
+ logger.info({
381
+ toolName: tool.name,
382
+ credentialReferenceId,
383
+ transportType: serverConfig.type,
384
+ headers: tool.headers,
385
+ }, 'Built MCP server config with credentials');
386
+ // Create and connect MCP client
387
+ const client = new McpClient({
388
+ name: tool.name,
389
+ server: serverConfig,
390
+ });
391
+ await client.connect();
392
+ return client.tools();
393
+ }
394
+ getFunctionTools(streamRequestId) {
395
+ if (!this.config.functionTools)
396
+ return {};
397
+ const functionTools = {};
398
+ for (const funcTool of this.config.functionTools) {
399
+ // Convert function tool to AI SDK format and wrap with streaming
400
+ const aiTool = tool({
401
+ description: funcTool.description,
402
+ inputSchema: funcTool.schema || z.object({}),
403
+ execute: funcTool.execute,
404
+ });
405
+ functionTools[funcTool.name] = this.wrapToolWithStreaming(funcTool.name, aiTool, streamRequestId, 'tool');
406
+ }
407
+ return functionTools;
408
+ }
409
+ /**
410
+ * Get resolved context using ContextResolver - will return cached data or fetch fresh data as needed
411
+ */
412
+ async getResolvedContext(conversationId, requestContext) {
413
+ try {
414
+ if (!this.config.contextConfigId) {
415
+ logger.debug({ graphId: this.config.graphId }, 'No context config found for graph');
416
+ return null;
417
+ }
418
+ // Get context configuration
419
+ const contextConfig = await getContextConfigById(dbClient)({
420
+ scopes: { tenantId: this.config.tenantId, projectId: this.config.projectId },
421
+ id: this.config.contextConfigId,
422
+ });
423
+ if (!contextConfig) {
424
+ logger.warn({ contextConfigId: this.config.contextConfigId }, 'Context config not found');
425
+ return null;
426
+ }
427
+ if (!this.contextResolver) {
428
+ throw new Error('Context resolver not found');
429
+ }
430
+ // Resolve context with 'invocation' trigger to ensure fresh data for invocation definitions
431
+ const result = await this.contextResolver.resolve(contextConfig, {
432
+ triggerEvent: 'invocation',
433
+ conversationId,
434
+ requestContext: requestContext || {},
435
+ tenantId: this.config.tenantId,
436
+ });
437
+ // Add built-in variables to resolved context
438
+ const contextWithBuiltins = {
439
+ ...result.resolvedContext,
440
+ $now: new Date().toISOString(),
441
+ $env: process.env,
442
+ };
443
+ logger.debug({
444
+ conversationId,
445
+ contextConfigId: contextConfig.id,
446
+ resolvedKeys: Object.keys(contextWithBuiltins),
447
+ cacheHits: result.cacheHits.length,
448
+ cacheMisses: result.cacheMisses.length,
449
+ fetchedDefinitions: result.fetchedDefinitions.length,
450
+ errors: result.errors.length,
451
+ }, 'Context resolved for agent');
452
+ return contextWithBuiltins;
453
+ }
454
+ catch (error) {
455
+ logger.error({
456
+ conversationId,
457
+ error: error instanceof Error ? error.message : 'Unknown error',
458
+ }, 'Failed to get resolved context');
459
+ return null;
460
+ }
461
+ }
462
+ /**
463
+ * Get the graph prompt for this agent's graph
464
+ */
465
+ async getGraphPrompt() {
466
+ try {
467
+ const graphDefinition = await getFullGraphDefinition(dbClient)({
468
+ scopes: {
469
+ tenantId: this.config.tenantId,
470
+ projectId: this.config.projectId,
471
+ },
472
+ graphId: this.config.graphId,
473
+ });
474
+ return graphDefinition?.graphPrompt || undefined;
475
+ }
476
+ catch (error) {
477
+ logger.warn({
478
+ graphId: this.config.graphId,
479
+ error: error instanceof Error ? error.message : 'Unknown error',
480
+ }, 'Failed to get graph prompt');
481
+ return undefined;
482
+ }
483
+ }
484
+ /**
485
+ * Check if any agent in the graph has artifact components configured
486
+ */
487
+ async hasGraphArtifactComponents() {
488
+ try {
489
+ const graphDefinition = await getFullGraphDefinition(dbClient)({
490
+ scopes: {
491
+ tenantId: this.config.tenantId,
492
+ projectId: this.config.projectId,
493
+ },
494
+ graphId: this.config.graphId,
495
+ });
496
+ if (!graphDefinition) {
497
+ return false;
498
+ }
499
+ // Check if artifactComponents exists and has any entries
500
+ return !!(graphDefinition.artifactComponents &&
501
+ Object.keys(graphDefinition.artifactComponents).length > 0);
502
+ }
503
+ catch (error) {
504
+ logger.warn({
505
+ graphId: this.config.graphId,
506
+ tenantId: this.config.tenantId,
507
+ projectId: this.config.projectId,
508
+ error: error instanceof Error ? error.message : 'Unknown error',
509
+ }, 'Failed to check graph artifact components, assuming none exist');
510
+ // Fallback to current agent's artifact components if graph query fails
511
+ return this.artifactComponents.length > 0;
512
+ }
513
+ }
514
+ /**
515
+ * Build adaptive system prompt for Phase 2 structured output generation
516
+ * based on configured data components and artifact components across the graph
517
+ */
518
+ async buildPhase2SystemPrompt() {
519
+ const hasDataComponents = this.config.dataComponents && this.config.dataComponents.length > 0;
520
+ const hasArtifactComponents = await this.hasGraphArtifactComponents();
521
+ if (hasDataComponents && hasArtifactComponents) {
522
+ return `Generate the final structured JSON response using the configured data components. Intersperse artifact references throughout the dataComponents array to support each piece of information with evidence from the research above. Use exact artifact_id and task_id values from the tool outputs - never make up or modify these IDs.
523
+
524
+ Key requirements:
525
+ - Mix artifact references throughout your dataComponents array
526
+ - Each artifact reference must use EXACT IDs from tool outputs
527
+ - Reference artifacts that directly support the adjacent information
528
+ - Follow the pattern: Data → Supporting Artifact → Next Data → Next Artifact`;
529
+ }
530
+ if (hasDataComponents && !hasArtifactComponents) {
531
+ return `Generate the final structured JSON response using the configured data components. Organize the information from the research above into the appropriate structured format based on the available component schemas.
532
+
533
+ Key requirements:
534
+ - Use the exact component structure and property names
535
+ - Fill in all relevant data from the research
536
+ - Ensure data is organized logically and completely`;
537
+ }
538
+ if (!hasDataComponents && hasArtifactComponents) {
539
+ return `Generate the final structured response with artifact references based on the research above. Use the artifact reference component to cite relevant information with exact artifact_id and task_id values from the tool outputs.
540
+
541
+ Key requirements:
542
+ - Use exact artifact_id and task_id from tool outputs
543
+ - Reference artifacts that support your response
544
+ - Never make up or modify artifact IDs`;
545
+ }
546
+ // Fallback case (shouldn't happen in normal operation since we check hasStructuredOutput)
547
+ return `Generate the final response based on the research above.`;
548
+ }
549
+ async buildSystemPrompt(runtimeContext, excludeDataComponents = false) {
550
+ // Get resolved context using ContextResolver
551
+ const conversationId = runtimeContext?.metadata?.conversationId || runtimeContext?.contextId;
552
+ // Set conversation ID if available
553
+ if (conversationId) {
554
+ this.setConversationId(conversationId);
555
+ }
556
+ const resolvedContext = conversationId ? await this.getResolvedContext(conversationId) : null;
557
+ // Process agent prompt with context
558
+ let processedPrompt = this.config.agentPrompt;
559
+ if (resolvedContext) {
560
+ try {
561
+ processedPrompt = TemplateEngine.render(this.config.agentPrompt, resolvedContext, { strict: false, preserveUnresolved: false });
562
+ }
563
+ catch (error) {
564
+ logger.error({
565
+ conversationId,
566
+ error: error instanceof Error ? error.message : 'Unknown error',
567
+ }, 'Failed to process agent prompt with context, using original');
568
+ processedPrompt = this.config.agentPrompt;
569
+ }
570
+ }
571
+ // Get MCP tools, function tools, and relational tools
572
+ const streamRequestId = runtimeContext?.metadata?.streamRequestId;
573
+ const mcpTools = await this.getMcpTools(undefined, streamRequestId);
574
+ const functionTools = this.getFunctionTools(streamRequestId);
575
+ const relationTools = this.getRelationTools(runtimeContext);
576
+ // Convert ToolSet objects to ToolData array format for system prompt
577
+ const allTools = { ...mcpTools, ...functionTools, ...relationTools };
578
+ const toolDefinitions = Object.entries(allTools).map(([name, tool]) => ({
579
+ name,
580
+ description: tool.description || '',
581
+ inputSchema: tool.inputSchema || tool.parameters || {},
582
+ usageGuidelines: name.startsWith('transfer_to_') || name.startsWith('delegate_to_')
583
+ ? `Use this tool to ${name.startsWith('transfer_to_') ? 'transfer' : 'delegate'} to another agent when appropriate.`
584
+ : 'Use this tool when appropriate for the task at hand.',
585
+ }));
586
+ const referenceTaskIds = await listTaskIdsByContextId(dbClient)({
587
+ contextId: runtimeContext?.contextId || '',
588
+ });
589
+ const referenceArtifacts = [];
590
+ for (const taskId of referenceTaskIds) {
591
+ const artifacts = await getLedgerArtifacts(dbClient)({
592
+ scopes: {
593
+ tenantId: this.config.tenantId,
594
+ projectId: this.config.projectId,
595
+ },
596
+ taskId: taskId,
597
+ });
598
+ referenceArtifacts.push(...artifacts);
599
+ }
600
+ // Use component dataComponents for system prompt (artifacts already separated in constructor)
601
+ const componentDataComponents = excludeDataComponents ? [] : this.config.dataComponents || [];
602
+ // Use thinking/preparation mode when we have data components but are excluding them (Phase 1)
603
+ const isThinkingPreparation = this.config.dataComponents && this.config.dataComponents.length > 0 && excludeDataComponents;
604
+ // Get graph prompt for additional context
605
+ let graphPrompt = await this.getGraphPrompt();
606
+ // Process graph prompt with context variables
607
+ if (graphPrompt && resolvedContext) {
608
+ try {
609
+ graphPrompt = TemplateEngine.render(graphPrompt, resolvedContext, {
610
+ strict: false,
611
+ preserveUnresolved: false,
612
+ });
613
+ }
614
+ catch (error) {
615
+ logger.error({
616
+ conversationId,
617
+ error: error instanceof Error ? error.message : 'Unknown error',
618
+ }, 'Failed to process graph prompt with context, using original');
619
+ // graphPrompt remains unchanged if processing fails
620
+ }
621
+ }
622
+ const config = {
623
+ corePrompt: processedPrompt,
624
+ graphPrompt,
625
+ tools: toolDefinitions,
626
+ dataComponents: componentDataComponents,
627
+ artifacts: referenceArtifacts,
628
+ isThinkingPreparation,
629
+ hasTransferRelations: (this.config.transferRelations?.length ?? 0) > 0,
630
+ hasDelegateRelations: (this.config.delegateRelations?.length ?? 0) > 0,
631
+ };
632
+ return await this.systemPromptBuilder.buildSystemPrompt(config);
633
+ }
634
+ getArtifactTools() {
635
+ return tool({
636
+ description: 'Call this tool to get the artifact with the given artifactId. Only retrieve this when the description of the artifact is insufficient to understand the artifact and you need to see the actual artifact for more context. Please refrain from using this tool unless absolutely necessary.',
637
+ inputSchema: z.object({
638
+ artifactId: z.string().describe('The unique identifier of the artifact to get.'),
639
+ }),
640
+ execute: async ({ artifactId }) => {
641
+ logger.info({ artifactId }, 'get_artifact executed');
642
+ const artifact = await getLedgerArtifacts(dbClient)({
643
+ scopes: {
644
+ tenantId: this.config.tenantId,
645
+ projectId: this.config.projectId,
646
+ },
647
+ artifactId,
648
+ });
649
+ if (!artifact) {
650
+ throw new Error(`Artifact ${artifactId} not found`);
651
+ }
652
+ return { artifact: artifact[0] };
653
+ },
654
+ });
655
+ }
656
+ // Create the thinking_complete tool to mark end of planning phase
657
+ createThinkingCompleteTool() {
658
+ return tool({
659
+ description: 'Call when research and planning is complete, ready to generate structured response. This marks the end of the thinking phase.',
660
+ inputSchema: z.object({
661
+ complete: z.boolean().describe('Set to true when research is finished'),
662
+ }),
663
+ execute: async (params) => params,
664
+ });
665
+ }
666
+ // Provide a default tool set that is always available to the agent.
667
+ async getDefaultTools(sessionId, streamRequestId) {
668
+ const defaultTools = {};
669
+ // Add get_reference_artifact if any agent in the graph has artifact components
670
+ // This enables cross-agent artifact collaboration within the same graph
671
+ if (await this.graphHasArtifactComponents()) {
672
+ defaultTools.get_reference_artifact = this.getArtifactTools();
673
+ }
674
+ // Only add save_tool_result if this specific agent has artifact components
675
+ if (this.artifactComponents.length > 0) {
676
+ defaultTools.save_tool_result = createSaveToolResultTool(sessionId, streamRequestId, this.config.id, this.artifactComponents);
677
+ }
678
+ // Add thinking_complete tool if we have structured output components
679
+ const hasStructuredOutput = this.config.dataComponents && this.config.dataComponents.length > 0;
680
+ if (hasStructuredOutput) {
681
+ const thinkingCompleteTool = this.createThinkingCompleteTool();
682
+ if (thinkingCompleteTool) {
683
+ defaultTools.thinking_complete = this.wrapToolWithStreaming('thinking_complete', thinkingCompleteTool, streamRequestId, 'tool');
684
+ }
685
+ }
686
+ return defaultTools;
687
+ }
688
+ // Check if any agents in the graph have artifact components
689
+ async graphHasArtifactComponents() {
690
+ try {
691
+ return await graphHasArtifactComponents(dbClient)({
692
+ scopes: {
693
+ tenantId: this.config.tenantId,
694
+ projectId: this.config.projectId,
695
+ },
696
+ graphId: this.config.graphId,
697
+ });
698
+ }
699
+ catch (error) {
700
+ logger.error({ error, graphId: this.config.graphId }, 'Failed to check graph artifact components');
701
+ return false;
702
+ }
703
+ }
704
+ async generate(userMessage, runtimeContext) {
705
+ return tracer.startActiveSpan(createSpanName('agent.generate'), async (span) => {
706
+ // Create tool session for this execution outside try blocks
707
+ const contextId = runtimeContext?.contextId || 'default';
708
+ const taskId = runtimeContext?.metadata?.taskId || 'unknown';
709
+ const sessionId = toolSessionManager.createSession(this.config.tenantId, this.config.projectId, contextId, taskId);
710
+ try {
711
+ // Set streaming helper from registry if available
712
+ const streamRequestId = runtimeContext?.metadata?.streamRequestId;
713
+ this.streamHelper = streamRequestId ? getStreamHelper(streamRequestId) : undefined;
714
+ const conversationId = runtimeContext?.metadata?.conversationId;
715
+ // Set conversation ID if available
716
+ if (conversationId) {
717
+ this.setConversationId(conversationId);
718
+ }
719
+ // Load all tools and both system prompts in parallel
720
+ // Note: getDefaultTools needs to be called after streamHelper is set above
721
+ const [mcpTools, systemPrompt, thinkingSystemPrompt, functionTools, relationTools, defaultTools,] = await tracer.startActiveSpan(createSpanName('agent.load_tools'), {
722
+ attributes: {
723
+ 'agent.name': this.config.name,
724
+ 'session.id': sessionId || 'none',
725
+ },
726
+ }, async (childSpan) => {
727
+ try {
728
+ const result = await Promise.all([
729
+ this.getMcpTools(sessionId, streamRequestId),
730
+ this.buildSystemPrompt(runtimeContext, false), // Normal prompt with data components
731
+ this.buildSystemPrompt(runtimeContext, true), // Thinking prompt without data components
732
+ Promise.resolve(this.getFunctionTools(streamRequestId)),
733
+ Promise.resolve(this.getRelationTools(runtimeContext, sessionId)),
734
+ this.getDefaultTools(sessionId, streamRequestId),
735
+ ]);
736
+ childSpan.setStatus({ code: SpanStatusCode.OK });
737
+ return result;
738
+ }
739
+ catch (err) {
740
+ // Use helper function for consistent error handling
741
+ handleSpanError(childSpan, err);
742
+ throw err;
743
+ }
744
+ finally {
745
+ childSpan.end();
746
+ // Force flush after critical tool loading span
747
+ await forceFlushTracer();
748
+ }
749
+ });
750
+ // Combine all tools for AI SDK
751
+ const allTools = {
752
+ ...mcpTools,
753
+ ...functionTools,
754
+ ...relationTools,
755
+ ...defaultTools,
756
+ };
757
+ // Get conversation history
758
+ let conversationHistory = '';
759
+ const historyConfig = this.config.conversationHistoryConfig ?? createDefaultConversationHistoryConfig();
760
+ if (historyConfig && historyConfig.mode !== 'none') {
761
+ if (historyConfig.mode === 'full') {
762
+ conversationHistory = await getFormattedConversationHistory({
763
+ tenantId: this.config.tenantId,
764
+ projectId: this.config.projectId,
765
+ conversationId: contextId,
766
+ currentMessage: userMessage,
767
+ options: historyConfig,
768
+ filters: {},
769
+ });
770
+ }
771
+ else if (historyConfig.mode === 'scoped') {
772
+ conversationHistory = await getFormattedConversationHistory({
773
+ tenantId: this.config.tenantId,
774
+ projectId: this.config.projectId,
775
+ conversationId: contextId,
776
+ currentMessage: userMessage,
777
+ options: historyConfig,
778
+ filters: {
779
+ agentId: this.config.id,
780
+ taskId: taskId,
781
+ },
782
+ });
783
+ }
784
+ }
785
+ // Use the primary model for text generation
786
+ const primaryModelSettings = this.getPrimaryModel();
787
+ const modelSettings = ModelFactory.prepareGenerationConfig(primaryModelSettings);
788
+ let response;
789
+ let textResponse;
790
+ // Check if we have structured output components
791
+ const hasStructuredOutput = this.config.dataComponents && this.config.dataComponents.length > 0;
792
+ // Phase 1: Stream only if no structured output needed
793
+ const shouldStreamPhase1 = this.getStreamingHelper() && !hasStructuredOutput;
794
+ // Extract maxDuration from config and convert to milliseconds, or use defaults
795
+ const timeoutMs = modelSettings.maxDuration
796
+ ? modelSettings.maxDuration * 1000
797
+ : shouldStreamPhase1
798
+ ? CONSTANTS.PHASE_1_TIMEOUT_MS
799
+ : CONSTANTS.NON_STREAMING_PHASE_1_TIMEOUT_MS;
800
+ // Build messages for Phase 1 - use thinking prompt if structured output needed
801
+ const phase1SystemPrompt = hasStructuredOutput ? thinkingSystemPrompt : systemPrompt;
802
+ const messages = [];
803
+ messages.push({ role: 'system', content: phase1SystemPrompt });
804
+ if (conversationHistory.trim() !== '') {
805
+ messages.push({ role: 'user', content: conversationHistory });
806
+ }
807
+ messages.push({
808
+ role: 'user',
809
+ content: userMessage,
810
+ });
811
+ // ----- PHASE 1: Planning with tools -----
812
+ if (shouldStreamPhase1) {
813
+ // Streaming Phase 1: Natural text + tools (no structured output needed)
814
+ const streamConfig = {
815
+ ...modelSettings,
816
+ toolChoice: 'auto', // Allow natural text + tools
817
+ };
818
+ // Use streamText for Phase 1 (text-only responses)
819
+ const streamResult = streamText({
820
+ ...streamConfig,
821
+ messages,
822
+ tools: allTools,
823
+ stopWhen: ({ steps }) => {
824
+ const last = steps.at(-1);
825
+ if (last && 'toolCalls' in last && last.toolCalls) {
826
+ return last.toolCalls.some((tc) => tc.toolName.startsWith('transfer_to_'));
827
+ }
828
+ // Safety cap at configured max steps
829
+ return steps.length >= this.getMaxGenerationSteps();
830
+ },
831
+ experimental_telemetry: {
832
+ isEnabled: true,
833
+ functionId: this.config.id,
834
+ recordInputs: true,
835
+ recordOutputs: true,
836
+ },
837
+ abortSignal: AbortSignal.timeout(timeoutMs),
838
+ });
839
+ // Create incremental parser that will format and stream to user
840
+ const streamHelper = this.getStreamingHelper();
841
+ if (!streamHelper) {
842
+ throw new Error('Stream helper is unexpectedly undefined in streaming context');
843
+ }
844
+ const parser = new IncrementalStreamParser(streamHelper, this.config.tenantId, contextId);
845
+ // Process the stream - text only (no structured output in Phase 1)
846
+ // Note: stopWhen will automatically stop on transfer_to_
847
+ for await (const textChunk of streamResult.textStream) {
848
+ await parser.processTextChunk(textChunk);
849
+ }
850
+ // Finalize the stream
851
+ await parser.finalize();
852
+ // Get the complete result for A2A protocol
853
+ response = await streamResult;
854
+ // Build formattedContent from collected parts
855
+ const collectedParts = parser.getCollectedParts();
856
+ if (collectedParts.length > 0) {
857
+ response.formattedContent = {
858
+ parts: collectedParts.map((part) => ({
859
+ kind: part.kind,
860
+ ...(part.kind === 'text' && { text: part.text }),
861
+ ...(part.kind === 'data' && { data: part.data }),
862
+ })),
863
+ };
864
+ }
865
+ }
866
+ else {
867
+ // Non-streaming Phase 1
868
+ let genConfig;
869
+ if (hasStructuredOutput) {
870
+ genConfig = {
871
+ ...modelSettings,
872
+ toolChoice: 'required', // Force tool usage, prevent text generation
873
+ };
874
+ }
875
+ else {
876
+ genConfig = {
877
+ ...modelSettings,
878
+ toolChoice: 'auto', // Allow both tools and text generation
879
+ };
880
+ }
881
+ // Use generateText for Phase 1 planning
882
+ response = await generateText({
883
+ ...genConfig,
884
+ messages,
885
+ tools: allTools,
886
+ stopWhen: ({ steps }) => {
887
+ const last = steps.at(-1);
888
+ if (last && 'toolCalls' in last && last.toolCalls) {
889
+ return last.toolCalls.some((tc) => tc.toolName.startsWith('transfer_to_') || tc.toolName === 'thinking_complete');
890
+ }
891
+ // Safety cap at configured max steps
892
+ return steps.length >= this.getMaxGenerationSteps();
893
+ },
894
+ experimental_telemetry: {
895
+ isEnabled: true,
896
+ functionId: this.config.id,
897
+ recordInputs: true,
898
+ recordOutputs: true,
899
+ metadata: {
900
+ phase: 'planning',
901
+ },
902
+ },
903
+ abortSignal: AbortSignal.timeout(timeoutMs),
904
+ });
905
+ }
906
+ // Resolve steps Promise so task handler can access the array properly
907
+ if (response.steps) {
908
+ const resolvedSteps = await response.steps;
909
+ response = { ...response, steps: resolvedSteps };
910
+ }
911
+ // ----- PHASE 2: Structured Output Generation -----
912
+ if (hasStructuredOutput && !hasToolCallWithPrefix('transfer_to_')(response)) {
913
+ // Check if thinking_complete was called (successful Phase 1)
914
+ const thinkingCompleteCall = response.steps
915
+ ?.flatMap((s) => s.toolCalls || [])
916
+ ?.find((tc) => tc.toolName === 'thinking_complete');
917
+ if (thinkingCompleteCall) {
918
+ // Build reasoning flow from Phase 1 steps
919
+ const reasoningFlow = [];
920
+ if (response.steps) {
921
+ response.steps.forEach((step) => {
922
+ // Add tool calls and results as formatted messages
923
+ if (step.toolCalls && step.toolResults) {
924
+ step.toolCalls.forEach((call, index) => {
925
+ const result = step.toolResults[index];
926
+ if (result) {
927
+ const storedResult = toolSessionManager.getToolResult(sessionId, result.toolCallId);
928
+ const toolName = storedResult?.toolName || call.toolName;
929
+ // Skip tool_thinking tool
930
+ if (toolName === 'thinking_complete') {
931
+ return;
932
+ }
933
+ // Special handling for save_artifact_tool and save_tool_result
934
+ if (toolName === 'save_artifact_tool' || toolName === 'save_tool_result') {
935
+ logger.info({ result }, 'save_artifact_tool or save_tool_result');
936
+ if (result.output.artifacts) {
937
+ for (const artifact of result.output.artifacts) {
938
+ const artifactId = artifact?.artifactId || 'N/A';
939
+ const taskId = artifact?.taskId || 'N/A';
940
+ const summaryData = artifact?.summaryData || {};
941
+ const formattedArtifact = `## Artifact Saved
942
+
943
+ **Artifact ID:** ${artifactId}
944
+ **Task ID:** ${taskId}
945
+
946
+ ### Summary
947
+ ${typeof summaryData === 'string' ? summaryData : JSON.stringify(summaryData, null, 2)}
948
+ `;
949
+ reasoningFlow.push({
950
+ role: 'assistant',
951
+ content: formattedArtifact,
952
+ });
953
+ }
954
+ }
955
+ return;
956
+ }
957
+ // Default formatting for all other tools
958
+ const actualResult = storedResult?.result || result.result || result;
959
+ const actualArgs = storedResult?.args || call.args;
960
+ const input = actualArgs ? JSON.stringify(actualArgs, null, 2) : 'No input';
961
+ const output = typeof actualResult === 'string'
962
+ ? actualResult
963
+ : JSON.stringify(actualResult, null, 2);
964
+ const formattedResult = `## Tool: ${call.toolName}
965
+
966
+ ### Input
967
+ ${input}
968
+
969
+ ### Output
970
+ ${output}`;
971
+ reasoningFlow.push({
972
+ role: 'assistant',
973
+ content: formattedResult,
974
+ });
975
+ }
976
+ });
977
+ }
978
+ });
979
+ }
980
+ // Build component schemas using reusable classes
981
+ const componentSchemas = [];
982
+ // Add data component schemas
983
+ if (this.config.dataComponents && this.config.dataComponents.length > 0) {
984
+ this.config.dataComponents.forEach((dc) => {
985
+ const propsSchema = jsonSchemaToZod(dc.props);
986
+ componentSchemas.push(z.object({
987
+ id: z.string(),
988
+ name: z.literal(dc.name),
989
+ props: propsSchema,
990
+ }));
991
+ });
992
+ }
993
+ // Add artifact reference schema
994
+ if (this.artifactComponents.length > 0) {
995
+ componentSchemas.push(ArtifactReferenceSchema.getSchema());
996
+ }
997
+ let dataComponentsSchema;
998
+ if (componentSchemas.length === 1) {
999
+ dataComponentsSchema = componentSchemas[0];
1000
+ }
1001
+ else {
1002
+ dataComponentsSchema = z.union(componentSchemas);
1003
+ }
1004
+ // Phase 2: Generate structured output
1005
+ const structuredModelSettings = ModelFactory.prepareGenerationConfig(this.getStructuredOutputModel());
1006
+ const phase2TimeoutMs = structuredModelSettings.maxDuration
1007
+ ? structuredModelSettings.maxDuration * 1000
1008
+ : CONSTANTS.PHASE_2_TIMEOUT_MS;
1009
+ const structuredResponse = await generateObject({
1010
+ ...structuredModelSettings,
1011
+ messages: [
1012
+ { role: 'user', content: userMessage },
1013
+ ...reasoningFlow,
1014
+ {
1015
+ role: 'system',
1016
+ content: await this.buildPhase2SystemPrompt(),
1017
+ },
1018
+ ],
1019
+ schema: z.object({
1020
+ dataComponents: z.array(dataComponentsSchema),
1021
+ }),
1022
+ experimental_telemetry: {
1023
+ isEnabled: true,
1024
+ functionId: this.config.id,
1025
+ recordInputs: true,
1026
+ recordOutputs: true,
1027
+ metadata: {
1028
+ phase: 'structured_generation',
1029
+ },
1030
+ },
1031
+ abortSignal: AbortSignal.timeout(phase2TimeoutMs),
1032
+ });
1033
+ // Merge structured output into response
1034
+ response = {
1035
+ ...response,
1036
+ object: structuredResponse.object,
1037
+ };
1038
+ textResponse = JSON.stringify(structuredResponse.object, null, 2);
1039
+ }
1040
+ else {
1041
+ textResponse = response.text || '';
1042
+ }
1043
+ }
1044
+ else {
1045
+ textResponse = response.steps[response.steps.length - 1].text || '';
1046
+ }
1047
+ // Mark span as successful
1048
+ span.setStatus({ code: SpanStatusCode.OK });
1049
+ span.end();
1050
+ // Force flush after critical agent generation span
1051
+ await forceFlushTracer();
1052
+ // Format response - handle object vs text responses differently
1053
+ // Only format if we don't already have formattedContent from streaming
1054
+ let formattedContent = response.formattedContent || null;
1055
+ if (!formattedContent) {
1056
+ if (response.object) {
1057
+ // For object responses, replace artifact markers and convert to parts array
1058
+ formattedContent = await this.responseFormatter.formatObjectResponse(response.object, contextId);
1059
+ }
1060
+ else if (textResponse) {
1061
+ // For text responses, apply artifact marker formatting to create text/data parts
1062
+ formattedContent = await this.responseFormatter.formatResponse(textResponse, contextId);
1063
+ }
1064
+ }
1065
+ const formattedResponse = {
1066
+ ...response,
1067
+ formattedContent: formattedContent,
1068
+ };
1069
+ // Record agent generation in GraphSession
1070
+ if (streamRequestId) {
1071
+ const generationType = response.object ? 'object_generation' : 'text_generation';
1072
+ graphSessionManager.recordEvent(streamRequestId, 'agent_generate', this.config.id, {
1073
+ parts: (formattedContent?.parts || []).map((part) => ({
1074
+ type: part.kind === 'text'
1075
+ ? 'text'
1076
+ : part.kind === 'data'
1077
+ ? 'tool_result'
1078
+ : 'text',
1079
+ content: part.text || JSON.stringify(part.data),
1080
+ })),
1081
+ generationType,
1082
+ });
1083
+ }
1084
+ // Clean up session after completion
1085
+ toolSessionManager.endSession(sessionId);
1086
+ return formattedResponse;
1087
+ }
1088
+ catch (error) {
1089
+ // Clean up session on error
1090
+ toolSessionManager.endSession(sessionId);
1091
+ // Record exception and mark span as error
1092
+ span.recordException(error);
1093
+ span.setStatus({
1094
+ code: SpanStatusCode.ERROR,
1095
+ message: error.message,
1096
+ });
1097
+ span.end();
1098
+ // Force flush after error to ensure error telemetry is sent
1099
+ await forceFlushTracer();
1100
+ getLogger('Agent').error(error, 'Agent generate error');
1101
+ throw error;
1102
+ }
1103
+ });
1104
+ }
1105
+ }