@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.
Files changed (109) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/cli/build.ts +8 -1
  3. package/cli/run.ts +8 -1
  4. package/dist/cli/build.js +8 -1
  5. package/dist/cli/run.js +8 -1
  6. package/dist/src/FlinkApp.d.ts +33 -0
  7. package/dist/src/FlinkApp.js +247 -27
  8. package/dist/src/FlinkContext.d.ts +21 -0
  9. package/dist/src/FlinkHttpHandler.d.ts +90 -1
  10. package/dist/src/TypeScriptCompiler.d.ts +42 -0
  11. package/dist/src/TypeScriptCompiler.js +366 -8
  12. package/dist/src/TypeScriptUtils.js +4 -0
  13. package/dist/src/ai/AgentRunner.d.ts +39 -0
  14. package/dist/src/ai/AgentRunner.js +625 -0
  15. package/dist/src/ai/FlinkAgent.d.ts +446 -0
  16. package/dist/src/ai/FlinkAgent.js +633 -0
  17. package/dist/src/ai/FlinkTool.d.ts +37 -0
  18. package/dist/src/ai/FlinkTool.js +2 -0
  19. package/dist/src/ai/LLMAdapter.d.ts +119 -0
  20. package/dist/src/ai/LLMAdapter.js +2 -0
  21. package/dist/src/ai/SubAgentExecutor.d.ts +36 -0
  22. package/dist/src/ai/SubAgentExecutor.js +220 -0
  23. package/dist/src/ai/ToolExecutor.d.ts +35 -0
  24. package/dist/src/ai/ToolExecutor.js +237 -0
  25. package/dist/src/ai/index.d.ts +5 -0
  26. package/dist/src/ai/index.js +21 -0
  27. package/dist/src/handlers/StreamWriterFactory.d.ts +20 -0
  28. package/dist/src/handlers/StreamWriterFactory.js +83 -0
  29. package/dist/src/index.d.ts +4 -0
  30. package/dist/src/index.js +4 -0
  31. package/dist/src/utils.d.ts +30 -0
  32. package/dist/src/utils.js +52 -0
  33. package/package.json +14 -2
  34. package/readme.md +425 -0
  35. package/spec/AgentDuplicateDetection.spec.ts +112 -0
  36. package/spec/AgentRunner.spec.ts +527 -0
  37. package/spec/ConversationHooks.spec.ts +290 -0
  38. package/spec/FlinkAgent.spec.ts +310 -0
  39. package/spec/FlinkApp.onError.spec.ts +1 -2
  40. package/spec/StreamingIntegration.spec.ts +138 -0
  41. package/spec/SubAgentSupport.spec.ts +941 -0
  42. package/spec/ToolExecutor.spec.ts +360 -0
  43. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar.js +57 -0
  44. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar2.js +59 -0
  45. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema.js +53 -0
  46. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema2.js +53 -0
  47. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema3.js +53 -0
  48. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema.js +55 -0
  49. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema2.js +55 -0
  50. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile.js +58 -0
  51. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile2.js +58 -0
  52. package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler.js +53 -0
  53. package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler2.js +55 -0
  54. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchCar.js +58 -0
  55. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOnboardingSession.js +76 -0
  56. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOrderWithComplexTypes.js +58 -0
  57. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchProductWithIntersection.js +59 -0
  58. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchUserWithUnion.js +59 -0
  59. package/spec/mock-project/dist/spec/mock-project/src/handlers/PostCar.js +55 -0
  60. package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogin.js +56 -0
  61. package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogout.js +55 -0
  62. package/spec/mock-project/dist/spec/mock-project/src/handlers/PutCar.js +55 -0
  63. package/spec/mock-project/dist/spec/mock-project/src/index.js +83 -0
  64. package/spec/mock-project/dist/spec/mock-project/src/repos/CarRepo.js +26 -0
  65. package/spec/mock-project/dist/spec/mock-project/src/schemas/Car.js +2 -0
  66. package/spec/mock-project/dist/spec/mock-project/src/schemas/DefaultExportSchema.js +2 -0
  67. package/spec/mock-project/dist/spec/mock-project/src/schemas/FileWithTwoSchemas.js +2 -0
  68. package/spec/mock-project/dist/src/FlinkApp.js +1012 -0
  69. package/spec/mock-project/dist/src/FlinkContext.js +2 -0
  70. package/spec/mock-project/dist/src/FlinkErrors.js +143 -0
  71. package/spec/mock-project/dist/src/FlinkHttpHandler.js +47 -0
  72. package/spec/mock-project/dist/src/FlinkJob.js +2 -0
  73. package/spec/mock-project/dist/src/FlinkLog.js +26 -0
  74. package/spec/mock-project/dist/src/FlinkPlugin.js +2 -0
  75. package/spec/mock-project/dist/src/FlinkRepo.js +224 -0
  76. package/spec/mock-project/dist/src/FlinkResponse.js +2 -0
  77. package/spec/mock-project/dist/src/ai/AgentExecutor.js +279 -0
  78. package/spec/mock-project/dist/src/ai/AgentRunner.js +625 -0
  79. package/spec/mock-project/dist/src/ai/FlinkAgent.js +633 -0
  80. package/spec/mock-project/dist/src/ai/FlinkTool.js +2 -0
  81. package/spec/mock-project/dist/src/ai/LLMAdapter.js +2 -0
  82. package/spec/mock-project/dist/src/ai/SubAgentExecutor.js +220 -0
  83. package/spec/mock-project/dist/src/ai/ToolExecutor.js +237 -0
  84. package/spec/mock-project/dist/src/auth/FlinkAuthPlugin.js +2 -0
  85. package/spec/mock-project/dist/src/auth/FlinkAuthUser.js +2 -0
  86. package/spec/mock-project/dist/src/handlers/StreamWriterFactory.js +83 -0
  87. package/spec/mock-project/dist/src/index.js +17 -69
  88. package/spec/mock-project/dist/src/mock-data-generator.js +9 -0
  89. package/spec/mock-project/dist/src/utils.js +290 -0
  90. package/spec/mock-project/tsconfig.json +6 -1
  91. package/spec/testHelpers.ts +49 -0
  92. package/spec/utils.caseConversion.spec.ts +80 -0
  93. package/spec/utils.spec.ts +13 -13
  94. package/src/FlinkApp.ts +251 -7
  95. package/src/FlinkContext.ts +22 -0
  96. package/src/FlinkHttpHandler.ts +100 -2
  97. package/src/TypeScriptCompiler.ts +420 -9
  98. package/src/TypeScriptUtils.ts +5 -0
  99. package/src/ai/AgentRunner.ts +549 -0
  100. package/src/ai/FlinkAgent.ts +770 -0
  101. package/src/ai/FlinkTool.ts +40 -0
  102. package/src/ai/LLMAdapter.ts +96 -0
  103. package/src/ai/SubAgentExecutor.ts +199 -0
  104. package/src/ai/ToolExecutor.ts +193 -0
  105. package/src/ai/index.ts +5 -0
  106. package/src/handlers/StreamWriterFactory.ts +84 -0
  107. package/src/index.ts +4 -0
  108. package/src/utils.ts +52 -0
  109. 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
+ }