@flink-app/flink 1.0.0 → 2.0.0-alpha.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/cli/build.ts +8 -1
- package/cli/run.ts +8 -1
- package/dist/cli/build.js +8 -1
- package/dist/cli/run.js +8 -1
- package/dist/src/FlinkApp.d.ts +33 -0
- package/dist/src/FlinkApp.js +247 -27
- package/dist/src/FlinkContext.d.ts +21 -0
- package/dist/src/FlinkHttpHandler.d.ts +90 -1
- package/dist/src/TypeScriptCompiler.d.ts +42 -0
- package/dist/src/TypeScriptCompiler.js +366 -8
- package/dist/src/TypeScriptUtils.js +4 -0
- package/dist/src/ai/AgentRunner.d.ts +39 -0
- package/dist/src/ai/AgentRunner.js +625 -0
- package/dist/src/ai/FlinkAgent.d.ts +446 -0
- package/dist/src/ai/FlinkAgent.js +633 -0
- package/dist/src/ai/FlinkTool.d.ts +37 -0
- package/dist/src/ai/FlinkTool.js +2 -0
- package/dist/src/ai/LLMAdapter.d.ts +119 -0
- package/dist/src/ai/LLMAdapter.js +2 -0
- package/dist/src/ai/SubAgentExecutor.d.ts +36 -0
- package/dist/src/ai/SubAgentExecutor.js +220 -0
- package/dist/src/ai/ToolExecutor.d.ts +35 -0
- package/dist/src/ai/ToolExecutor.js +237 -0
- package/dist/src/ai/index.d.ts +5 -0
- package/dist/src/ai/index.js +21 -0
- package/dist/src/handlers/StreamWriterFactory.d.ts +20 -0
- package/dist/src/handlers/StreamWriterFactory.js +83 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +4 -0
- package/dist/src/utils.d.ts +30 -0
- package/dist/src/utils.js +52 -0
- package/package.json +14 -2
- package/readme.md +425 -0
- package/spec/AgentDuplicateDetection.spec.ts +112 -0
- package/spec/AgentRunner.spec.ts +527 -0
- package/spec/ConversationHooks.spec.ts +290 -0
- package/spec/FlinkAgent.spec.ts +310 -0
- package/spec/FlinkApp.onError.spec.ts +1 -2
- package/spec/StreamingIntegration.spec.ts +138 -0
- package/spec/SubAgentSupport.spec.ts +941 -0
- package/spec/ToolExecutor.spec.ts +360 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar.js +57 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar2.js +59 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema.js +53 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema2.js +53 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema3.js +53 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema.js +55 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema2.js +55 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile.js +58 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile2.js +58 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler.js +53 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler2.js +55 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchCar.js +58 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOnboardingSession.js +76 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOrderWithComplexTypes.js +58 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchProductWithIntersection.js +59 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchUserWithUnion.js +59 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PostCar.js +55 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogin.js +56 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogout.js +55 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PutCar.js +55 -0
- package/spec/mock-project/dist/spec/mock-project/src/index.js +83 -0
- package/spec/mock-project/dist/spec/mock-project/src/repos/CarRepo.js +26 -0
- package/spec/mock-project/dist/spec/mock-project/src/schemas/Car.js +2 -0
- package/spec/mock-project/dist/spec/mock-project/src/schemas/DefaultExportSchema.js +2 -0
- package/spec/mock-project/dist/spec/mock-project/src/schemas/FileWithTwoSchemas.js +2 -0
- package/spec/mock-project/dist/src/FlinkApp.js +1012 -0
- package/spec/mock-project/dist/src/FlinkContext.js +2 -0
- package/spec/mock-project/dist/src/FlinkErrors.js +143 -0
- package/spec/mock-project/dist/src/FlinkHttpHandler.js +47 -0
- package/spec/mock-project/dist/src/FlinkJob.js +2 -0
- package/spec/mock-project/dist/src/FlinkLog.js +26 -0
- package/spec/mock-project/dist/src/FlinkPlugin.js +2 -0
- package/spec/mock-project/dist/src/FlinkRepo.js +224 -0
- package/spec/mock-project/dist/src/FlinkResponse.js +2 -0
- package/spec/mock-project/dist/src/ai/AgentExecutor.js +279 -0
- package/spec/mock-project/dist/src/ai/AgentRunner.js +625 -0
- package/spec/mock-project/dist/src/ai/FlinkAgent.js +633 -0
- package/spec/mock-project/dist/src/ai/FlinkTool.js +2 -0
- package/spec/mock-project/dist/src/ai/LLMAdapter.js +2 -0
- package/spec/mock-project/dist/src/ai/SubAgentExecutor.js +220 -0
- package/spec/mock-project/dist/src/ai/ToolExecutor.js +237 -0
- package/spec/mock-project/dist/src/auth/FlinkAuthPlugin.js +2 -0
- package/spec/mock-project/dist/src/auth/FlinkAuthUser.js +2 -0
- package/spec/mock-project/dist/src/handlers/StreamWriterFactory.js +83 -0
- package/spec/mock-project/dist/src/index.js +17 -69
- package/spec/mock-project/dist/src/mock-data-generator.js +9 -0
- package/spec/mock-project/dist/src/utils.js +290 -0
- package/spec/mock-project/tsconfig.json +6 -1
- package/spec/testHelpers.ts +49 -0
- package/spec/utils.caseConversion.spec.ts +80 -0
- package/spec/utils.spec.ts +13 -13
- package/src/FlinkApp.ts +251 -7
- package/src/FlinkContext.ts +22 -0
- package/src/FlinkHttpHandler.ts +100 -2
- package/src/TypeScriptCompiler.ts +420 -9
- package/src/TypeScriptUtils.ts +5 -0
- package/src/ai/AgentRunner.ts +549 -0
- package/src/ai/FlinkAgent.ts +770 -0
- package/src/ai/FlinkTool.ts +40 -0
- package/src/ai/LLMAdapter.ts +96 -0
- package/src/ai/SubAgentExecutor.ts +199 -0
- package/src/ai/ToolExecutor.ts +193 -0
- package/src/ai/index.ts +5 -0
- package/src/handlers/StreamWriterFactory.ts +84 -0
- package/src/index.ts +4 -0
- package/src/utils.ts +52 -0
- package/tsconfig.json +6 -1
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FlinkAgentProps,
|
|
3
|
+
AgentExecuteResult,
|
|
4
|
+
AgentExecuteInput,
|
|
5
|
+
StreamChunk,
|
|
6
|
+
Message,
|
|
7
|
+
AgentExecuteContext,
|
|
8
|
+
AgentStepContext,
|
|
9
|
+
AgentFinishContext,
|
|
10
|
+
} from "./FlinkAgent";
|
|
11
|
+
import { ToolExecutor } from "./ToolExecutor";
|
|
12
|
+
import { SubAgentExecutor } from "./SubAgentExecutor";
|
|
13
|
+
import { LLMAdapter, LLMMessage, LLMContentBlock, FlinkToolSchema } from "./LLMAdapter";
|
|
14
|
+
import { log } from "../FlinkLog";
|
|
15
|
+
|
|
16
|
+
export class AgentRunner {
|
|
17
|
+
private llmAdapter: LLMAdapter;
|
|
18
|
+
private maxTokens: number;
|
|
19
|
+
private temperature: number;
|
|
20
|
+
private maxSteps: number;
|
|
21
|
+
private timeoutMs: number;
|
|
22
|
+
private maxSubAgentDepth: number;
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
private agentProps: FlinkAgentProps,
|
|
26
|
+
private tools: Map<string, ToolExecutor<any>>,
|
|
27
|
+
llmAdapters: Map<string, LLMAdapter>,
|
|
28
|
+
private agentName?: string, // Optional agent name for logging
|
|
29
|
+
) {
|
|
30
|
+
// Get appropriate LLM adapter based on adapterId
|
|
31
|
+
const adapterId = agentProps.model?.adapterId || "default";
|
|
32
|
+
|
|
33
|
+
const adapter = llmAdapters.get(adapterId);
|
|
34
|
+
if (!adapter) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`LLM adapter "${adapterId}" not configured - register it in FlinkOptions.ai.llms`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.llmAdapter = adapter;
|
|
41
|
+
this.maxTokens = agentProps.model?.maxTokens || 4096;
|
|
42
|
+
this.temperature = agentProps.model?.temperature || 0.7;
|
|
43
|
+
this.maxSteps = agentProps.limits?.maxSteps || 10;
|
|
44
|
+
this.timeoutMs = agentProps.limits?.timeoutMs || 60000;
|
|
45
|
+
this.maxSubAgentDepth = agentProps.limits?.maxSubAgentDepth || 5;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Phase 1: Stream generator that yields complete event on finish
|
|
50
|
+
* Phase 2: Will yield text_delta and tool events during execution
|
|
51
|
+
*/
|
|
52
|
+
async *streamGenerator(
|
|
53
|
+
input: AgentExecuteInput,
|
|
54
|
+
): AsyncGenerator<StreamChunk> {
|
|
55
|
+
const maxSteps = input.options?.maxSteps || this.maxSteps;
|
|
56
|
+
const toolCalls: AgentExecuteResult["toolCalls"] = [];
|
|
57
|
+
const subAgentCalls: AgentExecuteResult["subAgentCalls"] = [];
|
|
58
|
+
|
|
59
|
+
// Get current depth from metadata, defaulting to 0 for root agents
|
|
60
|
+
const currentDepth = input.metadata?.subAgentDepth ?? 0;
|
|
61
|
+
|
|
62
|
+
// Check depth limit before execution
|
|
63
|
+
if (currentDepth > this.maxSubAgentDepth) {
|
|
64
|
+
// Build delegation chain for debugging
|
|
65
|
+
const chain = this.buildDelegationChain(input.metadata);
|
|
66
|
+
const chainStr = chain.length > 0 ? ` Delegation chain: ${chain.join(" → ")} → ${this.agentName || "unknown"}` : "";
|
|
67
|
+
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Sub-agent recursion depth limit exceeded (max: ${this.maxSubAgentDepth}, current: ${currentDepth}). ` +
|
|
70
|
+
`This usually indicates a circular delegation loop.${chainStr}`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Build execution context
|
|
75
|
+
const execContext: AgentExecuteContext = {
|
|
76
|
+
agentId: this.agentName || "unknown",
|
|
77
|
+
conversationId: input.conversationId,
|
|
78
|
+
user: input.user,
|
|
79
|
+
isSubAgent: input.metadata?.isSubAgentCall === true,
|
|
80
|
+
parentAgentId: input.metadata?.parentAgentId,
|
|
81
|
+
subAgentDepth: currentDepth,
|
|
82
|
+
metadata: input.metadata,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Initialize messages from history + new input
|
|
86
|
+
let messages: LLMMessage[];
|
|
87
|
+
|
|
88
|
+
if (input.history && input.history.length > 0) {
|
|
89
|
+
// Start with history
|
|
90
|
+
messages = this.convertMessages(input.history);
|
|
91
|
+
} else {
|
|
92
|
+
messages = [];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Add new user message
|
|
96
|
+
if (typeof input.message === "string") {
|
|
97
|
+
messages.push({ role: "user", content: input.message });
|
|
98
|
+
} else {
|
|
99
|
+
messages.push(...this.convertMessages(input.message));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let step = 0;
|
|
103
|
+
let finalMessage = "";
|
|
104
|
+
let stoppedEarly = false;
|
|
105
|
+
let totalInputTokens = 0;
|
|
106
|
+
let totalOutputTokens = 0;
|
|
107
|
+
|
|
108
|
+
while (step < maxSteps) {
|
|
109
|
+
step++;
|
|
110
|
+
|
|
111
|
+
// Filter tools based on user permissions (only show allowed tools to LLM)
|
|
112
|
+
const availableTools = await this.filterToolsByPermissions(input.user, input.userPermissions);
|
|
113
|
+
|
|
114
|
+
// Debug logging: Show what we're sending to the LLM
|
|
115
|
+
if (this.agentProps.debug) {
|
|
116
|
+
log.debug(`[Agent:${this.agentName}] Step ${step}/${maxSteps} - Calling LLM with:`, {
|
|
117
|
+
instructions: this.agentProps.instructions,
|
|
118
|
+
messageCount: messages.length,
|
|
119
|
+
messages: messages.map(m => ({
|
|
120
|
+
role: m.role,
|
|
121
|
+
contentPreview: typeof m.content === 'string'
|
|
122
|
+
? m.content.substring(0, 100) + (m.content.length > 100 ? '...' : '')
|
|
123
|
+
: `${(m.content as any[]).length} blocks`,
|
|
124
|
+
})),
|
|
125
|
+
toolCount: availableTools.length,
|
|
126
|
+
tools: availableTools.map(t => t.name),
|
|
127
|
+
maxTokens: this.maxTokens,
|
|
128
|
+
temperature: this.temperature,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Call AI model via adapter using streaming
|
|
133
|
+
const llmStream = this.llmAdapter.stream({
|
|
134
|
+
instructions: this.agentProps.instructions,
|
|
135
|
+
messages,
|
|
136
|
+
tools: availableTools,
|
|
137
|
+
maxTokens: this.maxTokens,
|
|
138
|
+
temperature: this.temperature,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Accumulate response from stream
|
|
142
|
+
let textContent = "";
|
|
143
|
+
const toolCallsFromStream: Array<{ id: string; name: string; input: any }> = [];
|
|
144
|
+
let usage = { inputTokens: 0, outputTokens: 0 };
|
|
145
|
+
let stopReason: "end_turn" | "tool_use" | "max_tokens" = "end_turn";
|
|
146
|
+
|
|
147
|
+
// Process streaming chunks
|
|
148
|
+
for await (const chunk of llmStream) {
|
|
149
|
+
switch (chunk.type) {
|
|
150
|
+
case "text":
|
|
151
|
+
textContent += chunk.delta;
|
|
152
|
+
// Yield text_delta event in real-time
|
|
153
|
+
yield { type: "text_delta", delta: chunk.delta };
|
|
154
|
+
break;
|
|
155
|
+
|
|
156
|
+
case "tool_call":
|
|
157
|
+
toolCallsFromStream.push(chunk.toolCall);
|
|
158
|
+
break;
|
|
159
|
+
|
|
160
|
+
case "usage":
|
|
161
|
+
usage = chunk.usage;
|
|
162
|
+
totalInputTokens += chunk.usage.inputTokens;
|
|
163
|
+
totalOutputTokens += chunk.usage.outputTokens;
|
|
164
|
+
break;
|
|
165
|
+
|
|
166
|
+
case "done":
|
|
167
|
+
stopReason = chunk.stopReason as "end_turn" | "tool_use" | "max_tokens";
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Create llmResponse structure for compatibility with existing code
|
|
173
|
+
const llmResponse = {
|
|
174
|
+
textContent: textContent || undefined,
|
|
175
|
+
toolCalls: toolCallsFromStream,
|
|
176
|
+
usage,
|
|
177
|
+
stopReason,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Debug logging: Show what the LLM responded with
|
|
181
|
+
if (this.agentProps.debug) {
|
|
182
|
+
log.debug(`[Agent:${this.agentName}] Step ${step} - LLM Response:`, {
|
|
183
|
+
textLength: llmResponse.textContent?.length || 0,
|
|
184
|
+
textPreview: llmResponse.textContent?.substring(0, 200) + (llmResponse.textContent && llmResponse.textContent.length > 200 ? '...' : ''),
|
|
185
|
+
toolCallsCount: llmResponse.toolCalls.length,
|
|
186
|
+
toolCalls: llmResponse.toolCalls.map(tc => ({
|
|
187
|
+
name: tc.name,
|
|
188
|
+
inputKeys: Object.keys(tc.input),
|
|
189
|
+
input: tc.input,
|
|
190
|
+
})),
|
|
191
|
+
stopReason: llmResponse.stopReason,
|
|
192
|
+
usage: llmResponse.usage,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Extract text response
|
|
197
|
+
if (llmResponse.textContent) {
|
|
198
|
+
finalMessage = llmResponse.textContent;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Build assistant message
|
|
202
|
+
const assistantContent: LLMContentBlock[] = [];
|
|
203
|
+
|
|
204
|
+
if (llmResponse.textContent) {
|
|
205
|
+
assistantContent.push({
|
|
206
|
+
type: "text",
|
|
207
|
+
text: llmResponse.textContent,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
for (const toolCall of llmResponse.toolCalls) {
|
|
212
|
+
assistantContent.push({
|
|
213
|
+
type: "tool_use",
|
|
214
|
+
id: toolCall.id,
|
|
215
|
+
name: toolCall.name,
|
|
216
|
+
input: toolCall.input,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
messages.push({
|
|
221
|
+
role: "assistant",
|
|
222
|
+
content: assistantContent,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Check for tool calls - if none, we're done after calling onStep
|
|
226
|
+
if (llmResponse.toolCalls.length === 0) {
|
|
227
|
+
// Call onStep hook before breaking
|
|
228
|
+
if (this.agentProps.onStep) {
|
|
229
|
+
const stepContext: AgentStepContext = {
|
|
230
|
+
...execContext,
|
|
231
|
+
step,
|
|
232
|
+
maxSteps,
|
|
233
|
+
messages: [...messages],
|
|
234
|
+
};
|
|
235
|
+
await this.agentProps.onStep(stepContext);
|
|
236
|
+
}
|
|
237
|
+
break; // No more tool calls - done
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Execute all tool calls
|
|
241
|
+
const toolResults: LLMContentBlock[] = [];
|
|
242
|
+
|
|
243
|
+
for (const toolCall of llmResponse.toolCalls) {
|
|
244
|
+
const toolExecutor = this.tools.get(toolCall.name);
|
|
245
|
+
let toolOutput: any;
|
|
246
|
+
let toolError: string | undefined;
|
|
247
|
+
|
|
248
|
+
// Debug logging: Tool execution start
|
|
249
|
+
if (this.agentProps.debug) {
|
|
250
|
+
log.debug(`[Agent:${this.agentName}] Executing tool '${toolCall.name}':`, {
|
|
251
|
+
input: toolCall.input,
|
|
252
|
+
inputSize: JSON.stringify(toolCall.input).length,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
if (!toolExecutor) {
|
|
258
|
+
throw new Error(`Tool ${toolCall.name} not found`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Check if this is a sub-agent executor
|
|
262
|
+
const isSubAgent =
|
|
263
|
+
toolExecutor instanceof SubAgentExecutor ||
|
|
264
|
+
(toolExecutor as any).isSubAgentExecutor?.();
|
|
265
|
+
|
|
266
|
+
if (isSubAgent) {
|
|
267
|
+
const subAgentId = (toolExecutor as any).getSubAgentId();
|
|
268
|
+
|
|
269
|
+
// Transform input if hook exists
|
|
270
|
+
let transformedInput = toolCall.input;
|
|
271
|
+
if (this.agentProps.transformSubAgentInput) {
|
|
272
|
+
try {
|
|
273
|
+
transformedInput = await this.agentProps.transformSubAgentInput(
|
|
274
|
+
subAgentId,
|
|
275
|
+
toolCall.input,
|
|
276
|
+
execContext
|
|
277
|
+
);
|
|
278
|
+
} catch (err: any) {
|
|
279
|
+
log.warn(`transformSubAgentInput hook failed for ${subAgentId}:`, err.message);
|
|
280
|
+
// Continue with original input if transformation fails
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Update the tool call input with transformed version
|
|
285
|
+
toolCall.input = transformedInput;
|
|
286
|
+
|
|
287
|
+
// Call onSubAgentCall hook if it exists (after transformation)
|
|
288
|
+
if (this.agentProps.onSubAgentCall) {
|
|
289
|
+
await this.agentProps.onSubAgentCall(
|
|
290
|
+
subAgentId,
|
|
291
|
+
transformedInput,
|
|
292
|
+
execContext
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Yield agent_call_start event
|
|
297
|
+
yield {
|
|
298
|
+
type: "agent_call_start",
|
|
299
|
+
agentId: subAgentId,
|
|
300
|
+
input: transformedInput,
|
|
301
|
+
};
|
|
302
|
+
} else {
|
|
303
|
+
// Yield tool_call_start for regular tools
|
|
304
|
+
yield {
|
|
305
|
+
type: "tool_call_start",
|
|
306
|
+
toolCall: {
|
|
307
|
+
id: toolCall.id,
|
|
308
|
+
name: toolCall.name,
|
|
309
|
+
input: toolCall.input,
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Pass parent context if this is a sub-agent executor
|
|
315
|
+
const toolResult = isSubAgent
|
|
316
|
+
? await (toolExecutor as any).execute(
|
|
317
|
+
toolCall.input,
|
|
318
|
+
input.user,
|
|
319
|
+
execContext, // Pass parent context to sub-agent
|
|
320
|
+
input.userPermissions, // Pass resolved permissions
|
|
321
|
+
)
|
|
322
|
+
: await toolExecutor.execute(toolCall.input, input.user, input.userPermissions);
|
|
323
|
+
|
|
324
|
+
// Format result for AI using new ToolResult format
|
|
325
|
+
const formattedResult = toolExecutor.formatResultForAI(toolResult);
|
|
326
|
+
|
|
327
|
+
toolResults.push({
|
|
328
|
+
type: "tool_result",
|
|
329
|
+
tool_use_id: toolCall.id,
|
|
330
|
+
content: formattedResult,
|
|
331
|
+
is_error: !toolResult.success,
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Track sub-agent calls separately
|
|
335
|
+
if (isSubAgent && toolResult.success) {
|
|
336
|
+
const subAgentId = (toolExecutor as any).getSubAgentId();
|
|
337
|
+
const agentResult = toolResult.data as AgentExecuteResult;
|
|
338
|
+
|
|
339
|
+
// Merge token usage from sub-agent
|
|
340
|
+
totalInputTokens += agentResult.usage?.inputTokens || 0;
|
|
341
|
+
totalOutputTokens += agentResult.usage?.outputTokens || 0;
|
|
342
|
+
|
|
343
|
+
subAgentCalls.push({
|
|
344
|
+
agentId: subAgentId,
|
|
345
|
+
input: toolCall.input,
|
|
346
|
+
result: agentResult,
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// Call onSubAgentComplete hook if it exists
|
|
350
|
+
if (this.agentProps.onSubAgentComplete) {
|
|
351
|
+
await this.agentProps.onSubAgentComplete(
|
|
352
|
+
subAgentId,
|
|
353
|
+
agentResult,
|
|
354
|
+
execContext
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Yield agent_call_result event
|
|
359
|
+
yield {
|
|
360
|
+
type: "agent_call_result",
|
|
361
|
+
agentId: subAgentId,
|
|
362
|
+
result: agentResult,
|
|
363
|
+
};
|
|
364
|
+
} else {
|
|
365
|
+
// Yield tool_call_result for regular tools
|
|
366
|
+
yield {
|
|
367
|
+
type: "tool_call_result",
|
|
368
|
+
toolCall: {
|
|
369
|
+
id: toolCall.id,
|
|
370
|
+
name: toolCall.name,
|
|
371
|
+
input: toolCall.input,
|
|
372
|
+
},
|
|
373
|
+
output: toolResult.success ? toolResult.data : null,
|
|
374
|
+
error: toolResult.success ? undefined : toolResult.error,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
toolCalls.push({
|
|
379
|
+
name: toolCall.name,
|
|
380
|
+
input: toolCall.input,
|
|
381
|
+
output: toolResult.success ? toolResult.data : null,
|
|
382
|
+
error: toolResult.success ? undefined : toolResult.error,
|
|
383
|
+
isAgentCall: isSubAgent,
|
|
384
|
+
agentId: isSubAgent ? (toolExecutor as any).getSubAgentId() : undefined,
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Debug logging: Tool execution result
|
|
388
|
+
if (this.agentProps.debug) {
|
|
389
|
+
log.debug(`[Agent:${this.agentName}] Tool '${toolCall.name}' ${toolResult.success ? 'succeeded' : 'failed'}:`, {
|
|
390
|
+
success: toolResult.success,
|
|
391
|
+
outputSize: toolResult.success ? JSON.stringify(toolResult.data).length : 0,
|
|
392
|
+
outputPreview: toolResult.success
|
|
393
|
+
? JSON.stringify(toolResult.data).substring(0, 200) + (JSON.stringify(toolResult.data).length > 200 ? '...' : '')
|
|
394
|
+
: toolResult.error,
|
|
395
|
+
code: toolResult.code,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (!toolResult.success) {
|
|
400
|
+
log.warn(`Tool ${toolCall.name} returned error:`, toolResult.error);
|
|
401
|
+
}
|
|
402
|
+
} catch (err: any) {
|
|
403
|
+
// Unexpected errors (not from tool itself)
|
|
404
|
+
toolError = err.message;
|
|
405
|
+
|
|
406
|
+
// Yield tool_call_result with error
|
|
407
|
+
yield {
|
|
408
|
+
type: "tool_call_result",
|
|
409
|
+
toolCall: {
|
|
410
|
+
id: toolCall.id,
|
|
411
|
+
name: toolCall.name,
|
|
412
|
+
input: toolCall.input,
|
|
413
|
+
},
|
|
414
|
+
output: null,
|
|
415
|
+
error: toolError,
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
toolResults.push({
|
|
419
|
+
type: "tool_result",
|
|
420
|
+
tool_use_id: toolCall.id,
|
|
421
|
+
content: `Error: ${err.message}`,
|
|
422
|
+
is_error: true,
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
toolCalls.push({
|
|
426
|
+
name: toolCall.name,
|
|
427
|
+
input: toolCall.input,
|
|
428
|
+
output: null,
|
|
429
|
+
error: toolError,
|
|
430
|
+
});
|
|
431
|
+
log.error(`Tool ${toolCall.name} execution failed:`, err.message);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Add tool results to conversation
|
|
436
|
+
messages.push({
|
|
437
|
+
role: "user",
|
|
438
|
+
content: toolResults,
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// Call onStep hook after each step
|
|
442
|
+
if (this.agentProps.onStep) {
|
|
443
|
+
const stepContext: AgentStepContext = {
|
|
444
|
+
...execContext,
|
|
445
|
+
step,
|
|
446
|
+
maxSteps,
|
|
447
|
+
messages: [...messages], // Clone to prevent mutation
|
|
448
|
+
};
|
|
449
|
+
await this.agentProps.onStep(stepContext);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (step >= maxSteps && toolCalls.length > 0) {
|
|
454
|
+
stoppedEarly = true;
|
|
455
|
+
log.warn(
|
|
456
|
+
`Agent ${this.agentName || "unknown"} stopped early after ${maxSteps} steps`,
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const result: AgentExecuteResult = {
|
|
461
|
+
message: finalMessage,
|
|
462
|
+
toolCalls,
|
|
463
|
+
stepsUsed: step,
|
|
464
|
+
stoppedEarly,
|
|
465
|
+
usage: { inputTokens: totalInputTokens, outputTokens: totalOutputTokens },
|
|
466
|
+
subAgentCalls: subAgentCalls.length > 0 ? subAgentCalls : undefined,
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
// Call afterRun hook with full context
|
|
470
|
+
if (this.agentProps.afterRun) {
|
|
471
|
+
const finishContext: AgentFinishContext = {
|
|
472
|
+
...execContext,
|
|
473
|
+
messages: [...messages],
|
|
474
|
+
result,
|
|
475
|
+
};
|
|
476
|
+
await this.agentProps.afterRun(result, finishContext);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Phase 1: Yield only complete event
|
|
480
|
+
// Phase 2: Will yield text_delta and tool events during loop
|
|
481
|
+
yield { type: "complete", result };
|
|
482
|
+
|
|
483
|
+
return result;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Convert Message[] to LLM message format
|
|
488
|
+
* Supports multi-turn conversations with history
|
|
489
|
+
*/
|
|
490
|
+
private convertMessages(messages: Message[]): LLMMessage[] {
|
|
491
|
+
return messages.map((m) => {
|
|
492
|
+
if (m.role === "user") {
|
|
493
|
+
return { role: "user" as const, content: m.content };
|
|
494
|
+
} else if (m.role === "assistant") {
|
|
495
|
+
return { role: "assistant" as const, content: m.content };
|
|
496
|
+
} else {
|
|
497
|
+
// For tool messages, convert to user message with tool result
|
|
498
|
+
return { role: "user" as const, content: m.result };
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
private getToolSchemas(): FlinkToolSchema[] {
|
|
504
|
+
return Array.from(this.tools.values()).map((t) => t.getToolSchema());
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Filter tools based on user permissions
|
|
509
|
+
* Only returns schemas for tools the user has permission to use
|
|
510
|
+
*
|
|
511
|
+
* @param user - User object
|
|
512
|
+
* @param userPermissions - Optional resolved permissions from auth plugin (preferred)
|
|
513
|
+
*/
|
|
514
|
+
private async filterToolsByPermissions(user?: any, userPermissions?: string[]): Promise<FlinkToolSchema[]> {
|
|
515
|
+
const allowedTools: FlinkToolSchema[] = [];
|
|
516
|
+
const toolExecutors = Array.from(this.tools.values());
|
|
517
|
+
|
|
518
|
+
for (const tool of toolExecutors) {
|
|
519
|
+
const hasPermission = await tool.checkPermissions(user, undefined, userPermissions);
|
|
520
|
+
if (hasPermission) {
|
|
521
|
+
allowedTools.push(tool.getToolSchema());
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return allowedTools;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Build delegation chain from metadata for error messages
|
|
530
|
+
* Extracts parent agent IDs to show the full call stack
|
|
531
|
+
*/
|
|
532
|
+
private buildDelegationChain(metadata?: Record<string, any>): string[] {
|
|
533
|
+
if (!metadata?.parentAgentId) {
|
|
534
|
+
return [];
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const chain: string[] = [];
|
|
538
|
+
let current = metadata;
|
|
539
|
+
|
|
540
|
+
// Walk up the parent chain
|
|
541
|
+
while (current?.parentAgentId) {
|
|
542
|
+
chain.unshift(current.parentAgentId);
|
|
543
|
+
// If parent metadata exists, continue walking up
|
|
544
|
+
current = current.parentMetadata;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return chain;
|
|
548
|
+
}
|
|
549
|
+
}
|