@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.
- package/README.md +117 -0
- package/dist/AgentExecutionServer.d.ts +23 -0
- package/dist/AgentExecutionServer.d.ts.map +1 -0
- package/dist/AgentExecutionServer.js +32 -0
- package/dist/__tests__/setup.d.ts +4 -0
- package/dist/__tests__/setup.d.ts.map +1 -0
- package/dist/__tests__/setup.js +50 -0
- package/dist/__tests__/utils/testProject.d.ts +18 -0
- package/dist/__tests__/utils/testProject.d.ts.map +1 -0
- package/dist/__tests__/utils/testProject.js +26 -0
- package/dist/__tests__/utils/testRequest.d.ts +8 -0
- package/dist/__tests__/utils/testRequest.d.ts.map +1 -0
- package/dist/__tests__/utils/testRequest.js +32 -0
- package/dist/__tests__/utils/testTenant.d.ts +64 -0
- package/dist/__tests__/utils/testTenant.d.ts.map +1 -0
- package/dist/__tests__/utils/testTenant.js +71 -0
- package/dist/a2a/client.d.ts +182 -0
- package/dist/a2a/client.d.ts.map +1 -0
- package/dist/a2a/client.js +645 -0
- package/dist/a2a/handlers.d.ts +4 -0
- package/dist/a2a/handlers.d.ts.map +1 -0
- package/dist/a2a/handlers.js +657 -0
- package/dist/a2a/transfer.d.ts +18 -0
- package/dist/a2a/transfer.d.ts.map +1 -0
- package/dist/a2a/transfer.js +22 -0
- package/dist/a2a/types.d.ts +63 -0
- package/dist/a2a/types.d.ts.map +1 -0
- package/dist/a2a/types.js +1 -0
- package/dist/agents/Agent.d.ts +154 -0
- package/dist/agents/Agent.d.ts.map +1 -0
- package/dist/agents/Agent.js +1105 -0
- package/dist/agents/ModelFactory.d.ts +62 -0
- package/dist/agents/ModelFactory.d.ts.map +1 -0
- package/dist/agents/ModelFactory.js +208 -0
- package/dist/agents/SystemPromptBuilder.d.ts +14 -0
- package/dist/agents/SystemPromptBuilder.d.ts.map +1 -0
- package/dist/agents/SystemPromptBuilder.js +62 -0
- package/dist/agents/ToolSessionManager.d.ts +61 -0
- package/dist/agents/ToolSessionManager.d.ts.map +1 -0
- package/dist/agents/ToolSessionManager.js +143 -0
- package/dist/agents/artifactTools.d.ts +30 -0
- package/dist/agents/artifactTools.d.ts.map +1 -0
- package/dist/agents/artifactTools.js +463 -0
- package/dist/agents/generateTaskHandler.d.ts +41 -0
- package/dist/agents/generateTaskHandler.d.ts.map +1 -0
- package/dist/agents/generateTaskHandler.js +350 -0
- package/dist/agents/relationTools.d.ts +33 -0
- package/dist/agents/relationTools.d.ts.map +1 -0
- package/dist/agents/relationTools.js +245 -0
- package/dist/agents/types.d.ts +23 -0
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/agents/types.js +1 -0
- package/dist/agents/versions/V1Config.d.ts +21 -0
- package/dist/agents/versions/V1Config.d.ts.map +1 -0
- package/dist/agents/versions/V1Config.js +285 -0
- package/dist/app.d.ts +4 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +194 -0
- package/dist/data/agentGraph.d.ts +4 -0
- package/dist/data/agentGraph.d.ts.map +1 -0
- package/dist/data/agentGraph.js +73 -0
- package/dist/data/agents.d.ts +4 -0
- package/dist/data/agents.d.ts.map +1 -0
- package/dist/data/agents.js +73 -0
- package/dist/data/conversations.d.ts +59 -0
- package/dist/data/conversations.d.ts.map +1 -0
- package/dist/data/conversations.js +216 -0
- package/dist/data/db/clean.d.ts +6 -0
- package/dist/data/db/clean.d.ts.map +1 -0
- package/dist/data/db/clean.js +77 -0
- package/dist/data/db/dbClient.d.ts +3 -0
- package/dist/data/db/dbClient.d.ts.map +1 -0
- package/dist/data/db/dbClient.js +13 -0
- package/dist/env.d.ts +43 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +63 -0
- package/dist/handlers/executionHandler.d.ts +36 -0
- package/dist/handlers/executionHandler.d.ts.map +1 -0
- package/dist/handlers/executionHandler.js +402 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/instrumentation.d.ts +13 -0
- package/dist/instrumentation.d.ts.map +1 -0
- package/dist/instrumentation.js +66 -0
- package/dist/logger.d.ts +4 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +32 -0
- package/dist/middleware/api-key-auth.d.ts +22 -0
- package/dist/middleware/api-key-auth.d.ts.map +1 -0
- package/dist/middleware/api-key-auth.js +139 -0
- package/dist/middleware/index.d.ts +2 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +1 -0
- package/dist/openapi.d.ts +2 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/openapi.js +36 -0
- package/dist/routes/agents.d.ts +4 -0
- package/dist/routes/agents.d.ts.map +1 -0
- package/dist/routes/agents.js +155 -0
- package/dist/routes/chat.d.ts +4 -0
- package/dist/routes/chat.d.ts.map +1 -0
- package/dist/routes/chat.js +308 -0
- package/dist/routes/chatDataStream.d.ts +4 -0
- package/dist/routes/chatDataStream.d.ts.map +1 -0
- package/dist/routes/chatDataStream.js +179 -0
- package/dist/routes/mcp.d.ts +4 -0
- package/dist/routes/mcp.d.ts.map +1 -0
- package/dist/routes/mcp.js +500 -0
- package/dist/tracer.d.ts +24 -0
- package/dist/tracer.d.ts.map +1 -0
- package/dist/tracer.js +97 -0
- package/dist/types/chat.d.ts +25 -0
- package/dist/types/chat.d.ts.map +1 -0
- package/dist/types/chat.js +1 -0
- package/dist/types/execution-context.d.ts +14 -0
- package/dist/types/execution-context.d.ts.map +1 -0
- package/dist/types/execution-context.js +14 -0
- package/dist/utils/agent-operations.d.ts +79 -0
- package/dist/utils/agent-operations.d.ts.map +1 -0
- package/dist/utils/agent-operations.js +67 -0
- package/dist/utils/artifact-component-schema.d.ts +29 -0
- package/dist/utils/artifact-component-schema.d.ts.map +1 -0
- package/dist/utils/artifact-component-schema.js +119 -0
- package/dist/utils/artifact-parser.d.ts +71 -0
- package/dist/utils/artifact-parser.d.ts.map +1 -0
- package/dist/utils/artifact-parser.js +251 -0
- package/dist/utils/cleanup.d.ts +19 -0
- package/dist/utils/cleanup.d.ts.map +1 -0
- package/dist/utils/cleanup.js +66 -0
- package/dist/utils/data-component-schema.d.ts +6 -0
- package/dist/utils/data-component-schema.d.ts.map +1 -0
- package/dist/utils/data-component-schema.js +43 -0
- package/dist/utils/graph-session.d.ts +200 -0
- package/dist/utils/graph-session.d.ts.map +1 -0
- package/dist/utils/graph-session.js +1009 -0
- package/dist/utils/incremental-stream-parser.d.ts +57 -0
- package/dist/utils/incremental-stream-parser.d.ts.map +1 -0
- package/dist/utils/incremental-stream-parser.js +287 -0
- package/dist/utils/response-formatter.d.ts +27 -0
- package/dist/utils/response-formatter.d.ts.map +1 -0
- package/dist/utils/response-formatter.js +160 -0
- package/dist/utils/stream-helpers.d.ts +162 -0
- package/dist/utils/stream-helpers.d.ts.map +1 -0
- package/dist/utils/stream-helpers.js +385 -0
- package/dist/utils/stream-registry.d.ts +18 -0
- package/dist/utils/stream-registry.d.ts.map +1 -0
- package/dist/utils/stream-registry.js +33 -0
- 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
|
+
}
|