@flink-app/flink 0.14.3 → 2.0.0-alpha.100

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 (280) hide show
  1. package/CHANGELOG.md +1051 -0
  2. package/SCHEMA_EXTRACTION_ANALYSIS.md +494 -0
  3. package/SIMPLE_AST_FEASIBILITY.md +570 -0
  4. package/bin/flink.ts +13 -2
  5. package/cli/build.ts +24 -44
  6. package/cli/clean.ts +13 -25
  7. package/cli/cli-utils.ts +190 -17
  8. package/cli/dev.ts +252 -0
  9. package/cli/loadEnvFiles.ts +116 -0
  10. package/cli/run.ts +45 -62
  11. package/dist/bin/flink.js +61 -2
  12. package/dist/cli/build.js +20 -25
  13. package/dist/cli/clean.js +12 -10
  14. package/dist/cli/cli-utils.d.ts +34 -3
  15. package/dist/cli/cli-utils.js +193 -12
  16. package/dist/cli/dev.d.ts +2 -0
  17. package/dist/cli/dev.js +279 -0
  18. package/dist/cli/loadEnvFiles.d.ts +30 -0
  19. package/dist/cli/loadEnvFiles.js +113 -0
  20. package/dist/cli/run.js +47 -46
  21. package/dist/src/DependencyTracker.d.ts +44 -0
  22. package/dist/src/DependencyTracker.js +239 -0
  23. package/dist/src/FlinkApp.d.ts +163 -10
  24. package/dist/src/FlinkApp.js +847 -184
  25. package/dist/src/FlinkContext.d.ts +41 -0
  26. package/dist/src/FlinkErrors.d.ts +19 -6
  27. package/dist/src/FlinkErrors.js +36 -42
  28. package/dist/src/FlinkHttpHandler.d.ts +219 -26
  29. package/dist/src/FlinkHttpHandler.js +37 -1
  30. package/dist/src/FlinkJob.d.ts +10 -0
  31. package/dist/src/FlinkLog.d.ts +82 -18
  32. package/dist/src/FlinkLog.js +165 -13
  33. package/dist/src/FlinkLogFactory.d.ts +288 -0
  34. package/dist/src/FlinkLogFactory.js +619 -0
  35. package/dist/src/FlinkRepo.d.ts +10 -2
  36. package/dist/src/FlinkRepo.js +11 -1
  37. package/dist/src/FlinkRequestContext.d.ts +63 -0
  38. package/dist/src/FlinkRequestContext.js +74 -0
  39. package/dist/src/FlinkResponse.d.ts +6 -0
  40. package/dist/src/FlinkService.d.ts +38 -0
  41. package/dist/src/FlinkService.js +46 -0
  42. package/dist/src/LeaderElection.d.ts +45 -0
  43. package/dist/src/LeaderElection.js +269 -0
  44. package/dist/src/SchemaCache.d.ts +84 -0
  45. package/dist/src/SchemaCache.js +289 -0
  46. package/dist/src/TypeScriptCompiler.d.ts +161 -51
  47. package/dist/src/TypeScriptCompiler.js +1253 -617
  48. package/dist/src/TypeScriptUtils.js +4 -0
  49. package/dist/src/ai/AgentRunner.d.ts +39 -0
  50. package/dist/src/ai/AgentRunner.js +760 -0
  51. package/dist/src/ai/ConversationAgent.d.ts +279 -0
  52. package/dist/src/ai/ConversationAgent.js +404 -0
  53. package/dist/src/ai/ConversationFlinkAgent.d.ts +278 -0
  54. package/dist/src/ai/ConversationFlinkAgent.js +404 -0
  55. package/dist/src/ai/FlinkAgent.d.ts +690 -0
  56. package/dist/src/ai/FlinkAgent.js +729 -0
  57. package/dist/src/ai/FlinkTool.d.ts +135 -0
  58. package/dist/src/ai/FlinkTool.js +2 -0
  59. package/dist/src/ai/InMemoryConversationAgent.d.ts +121 -0
  60. package/dist/src/ai/InMemoryConversationAgent.js +209 -0
  61. package/dist/src/ai/LLMAdapter.d.ts +148 -0
  62. package/dist/src/ai/LLMAdapter.js +2 -0
  63. package/dist/src/ai/PersistentFlinkAgent.d.ts +278 -0
  64. package/dist/src/ai/PersistentFlinkAgent.js +403 -0
  65. package/dist/src/ai/SubAgentExecutor.d.ts +38 -0
  66. package/dist/src/ai/SubAgentExecutor.js +223 -0
  67. package/dist/src/ai/ToolExecutor.d.ts +64 -0
  68. package/dist/src/ai/ToolExecutor.js +497 -0
  69. package/dist/src/ai/agentInstructions.d.ts +68 -0
  70. package/dist/src/ai/agentInstructions.js +286 -0
  71. package/dist/src/ai/index.d.ts +8 -0
  72. package/dist/src/ai/index.js +26 -0
  73. package/dist/src/ai/instructionFileLoader.d.ts +44 -0
  74. package/dist/src/ai/instructionFileLoader.js +179 -0
  75. package/dist/src/auth/FlinkAuthPlugin.d.ts +1 -1
  76. package/dist/src/handlers/StreamWriterFactory.d.ts +20 -0
  77. package/dist/src/handlers/StreamWriterFactory.js +83 -0
  78. package/dist/src/index.d.ts +14 -0
  79. package/dist/src/index.js +17 -0
  80. package/dist/src/loadPluginSchemas.d.ts +45 -0
  81. package/dist/src/loadPluginSchemas.js +143 -0
  82. package/dist/src/schema-extraction/ComplexTypeDetection.d.ts +40 -0
  83. package/dist/src/schema-extraction/ComplexTypeDetection.js +75 -0
  84. package/dist/src/schema-extraction/TypeScriptSourceParser.d.ts +321 -0
  85. package/dist/src/schema-extraction/TypeScriptSourceParser.js +925 -0
  86. package/dist/src/schema-extraction/TypeScriptSourceParser.spec.d.ts +1 -0
  87. package/dist/src/schema-extraction/TypeScriptSourceParser.spec.js +233 -0
  88. package/dist/src/schema-extraction/TypeScriptTokenizer.d.ts +57 -0
  89. package/dist/src/schema-extraction/TypeScriptTokenizer.js +177 -0
  90. package/dist/src/schema-extraction/index.d.ts +2 -0
  91. package/dist/src/schema-extraction/index.js +20 -0
  92. package/dist/src/schema-extraction/types.d.ts +31 -0
  93. package/dist/src/schema-extraction/types.js +2 -0
  94. package/dist/src/utils/loadFlinkConfig.d.ts +53 -0
  95. package/dist/src/utils/loadFlinkConfig.js +77 -0
  96. package/dist/src/utils.d.ts +30 -0
  97. package/dist/src/utils.js +52 -0
  98. package/dist/src/workers/SchemaGeneratorWorker.d.ts +1 -0
  99. package/dist/src/workers/SchemaGeneratorWorker.js +49 -0
  100. package/dist/src/workers/WorkerPool.d.ts +60 -0
  101. package/dist/src/workers/WorkerPool.js +306 -0
  102. package/examples/logging-hierarchical-example.ts +125 -0
  103. package/package.json +29 -4
  104. package/readme.md +499 -0
  105. package/spec/AgentDescendantDetection.spec.ts +335 -0
  106. package/spec/AgentDuplicateDetection.spec.ts +112 -0
  107. package/spec/AgentObserver.spec.ts +266 -0
  108. package/spec/AgentRunner.spec.ts +1062 -0
  109. package/spec/AsyncLocalStorageContext.spec.ts +223 -0
  110. package/spec/ConversationHooks.spec.ts +257 -0
  111. package/spec/FlinkAgent.spec.ts +681 -0
  112. package/spec/FlinkApp.htmlResponse.spec.ts +260 -0
  113. package/spec/FlinkApp.onError.invocation.spec.ts +151 -0
  114. package/spec/FlinkApp.onError.spec.ts +1 -2
  115. package/spec/FlinkApp.query.spec.ts +107 -0
  116. package/spec/FlinkApp.routeOrdering.spec.ts +61 -0
  117. package/spec/FlinkApp.undefinedResponse.spec.ts +123 -0
  118. package/spec/FlinkApp.validationMode.spec.ts +155 -0
  119. package/spec/FlinkJob.spec.ts +171 -0
  120. package/spec/FlinkLogFactory.spec.ts +337 -0
  121. package/spec/FlinkRepo.spec.ts +1 -1
  122. package/spec/LeaderElection.spec.ts +174 -0
  123. package/spec/StreamingIntegration.spec.ts +139 -0
  124. package/spec/ToolExecutor.spec.ts +465 -0
  125. package/spec/TypeScriptCompiler.spec.ts +1 -1
  126. package/spec/TypeScriptSourceParser.spec.ts +1215 -0
  127. package/spec/TypeScriptTokenizer.spec.ts +366 -0
  128. package/spec/ai/ContextCompaction.spec.ts +405 -0
  129. package/spec/ai/ConversationAgent.spec.ts +520 -0
  130. package/spec/ai/InMemoryConversationAgent.spec.ts +144 -0
  131. package/spec/ai/agentInstructions.spec.ts +358 -0
  132. package/spec/fixtures/agent-instructions/TestAgent.ts +24 -0
  133. package/spec/fixtures/agent-instructions/simple.md +3 -0
  134. package/spec/fixtures/agent-instructions/template.md +18 -0
  135. package/spec/fixtures/agent-instructions/yaml-format.yaml +9 -0
  136. package/spec/mock-project/dist/.tsbuildinfo +1 -0
  137. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar.js +56 -0
  138. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar2.js +58 -0
  139. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema.js +52 -0
  140. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema2.js +52 -0
  141. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema3.js +52 -0
  142. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema.js +54 -0
  143. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema2.js +54 -0
  144. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile.js +57 -0
  145. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile2.js +57 -0
  146. package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler.js +53 -0
  147. package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler2.js +55 -0
  148. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchCar.js +57 -0
  149. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOnboardingSession.js +75 -0
  150. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOrderWithComplexTypes.js +57 -0
  151. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchProductWithIntersection.js +58 -0
  152. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchUserWithUnion.js +58 -0
  153. package/spec/mock-project/dist/spec/mock-project/src/handlers/PostCar.js +54 -0
  154. package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogin.js +55 -0
  155. package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogout.js +54 -0
  156. package/spec/mock-project/dist/spec/mock-project/src/handlers/PutCar.js +54 -0
  157. package/spec/mock-project/dist/spec/mock-project/src/index.js +83 -0
  158. package/spec/mock-project/dist/spec/mock-project/src/repos/CarRepo.js +26 -0
  159. package/spec/mock-project/dist/spec/mock-project/src/schemas/Car.js +2 -0
  160. package/spec/mock-project/dist/spec/mock-project/src/schemas/DefaultExportSchema.js +2 -0
  161. package/spec/mock-project/dist/spec/mock-project/src/schemas/FileWithTwoSchemas.js +2 -0
  162. package/spec/mock-project/dist/src/FlinkApp.js +1000 -0
  163. package/spec/mock-project/dist/src/FlinkContext.js +2 -0
  164. package/spec/mock-project/dist/src/FlinkErrors.js +143 -0
  165. package/spec/mock-project/dist/src/FlinkHttpHandler.js +47 -0
  166. package/spec/mock-project/dist/src/FlinkJob.js +2 -0
  167. package/spec/mock-project/dist/src/FlinkLog.js +119 -0
  168. package/spec/mock-project/dist/src/FlinkLogFactory.js +617 -0
  169. package/spec/mock-project/dist/src/FlinkPlugin.js +2 -0
  170. package/spec/mock-project/dist/src/FlinkRepo.js +224 -0
  171. package/spec/mock-project/dist/src/FlinkRequestContext.js +74 -0
  172. package/spec/mock-project/dist/src/FlinkResponse.js +2 -0
  173. package/spec/mock-project/dist/src/ai/AgentExecutor.js +279 -0
  174. package/spec/mock-project/dist/src/ai/AgentRunner.js +632 -0
  175. package/spec/mock-project/dist/src/ai/ConversationAgent.js +402 -0
  176. package/spec/mock-project/dist/src/ai/ConversationFlinkAgent.js +422 -0
  177. package/spec/mock-project/dist/src/ai/FlinkAgent.js +699 -0
  178. package/spec/mock-project/dist/src/ai/FlinkTool.js +2 -0
  179. package/spec/mock-project/dist/src/ai/InMemoryConversationAgent.js +209 -0
  180. package/spec/mock-project/dist/src/ai/LLMAdapter.js +2 -0
  181. package/spec/mock-project/dist/src/ai/SubAgentExecutor.js +223 -0
  182. package/spec/mock-project/dist/src/ai/ToolExecutor.js +412 -0
  183. package/spec/mock-project/dist/src/ai/agentInstructions.js +246 -0
  184. package/spec/mock-project/dist/src/auth/FlinkAuthPlugin.js +2 -0
  185. package/spec/mock-project/dist/src/auth/FlinkAuthUser.js +2 -0
  186. package/spec/mock-project/dist/src/handlers/GetCar.js +26 -52
  187. package/spec/mock-project/dist/src/handlers/GetCar.js.map +1 -0
  188. package/spec/mock-project/dist/src/handlers/GetCar2.js +32 -54
  189. package/spec/mock-project/dist/src/handlers/GetCar2.js.map +1 -0
  190. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema.js +26 -48
  191. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema.js.map +1 -0
  192. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema2.js +28 -48
  193. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema2.js.map +1 -0
  194. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema3.js +29 -48
  195. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema3.js.map +1 -0
  196. package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema.js +26 -50
  197. package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema.js.map +1 -0
  198. package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema2.js +28 -50
  199. package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema2.js.map +1 -0
  200. package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile.js +27 -53
  201. package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile.js.map +1 -0
  202. package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile2.js +29 -53
  203. package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile2.js.map +1 -0
  204. package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler.js +16 -49
  205. package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler.js.map +1 -0
  206. package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler2.js +25 -50
  207. package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler2.js.map +1 -0
  208. package/spec/mock-project/dist/src/handlers/PatchCar.js +27 -53
  209. package/spec/mock-project/dist/src/handlers/PatchCar.js.map +1 -0
  210. package/spec/mock-project/dist/src/handlers/PatchOnboardingSession.js +44 -70
  211. package/spec/mock-project/dist/src/handlers/PatchOnboardingSession.js.map +1 -0
  212. package/spec/mock-project/dist/src/handlers/PatchOrderWithComplexTypes.js +27 -53
  213. package/spec/mock-project/dist/src/handlers/PatchOrderWithComplexTypes.js.map +1 -0
  214. package/spec/mock-project/dist/src/handlers/PatchProductWithIntersection.js +28 -54
  215. package/spec/mock-project/dist/src/handlers/PatchProductWithIntersection.js.map +1 -0
  216. package/spec/mock-project/dist/src/handlers/PatchUserWithUnion.js +28 -54
  217. package/spec/mock-project/dist/src/handlers/PatchUserWithUnion.js.map +1 -0
  218. package/spec/mock-project/dist/src/handlers/PostCar.js +24 -50
  219. package/spec/mock-project/dist/src/handlers/PostCar.js.map +1 -0
  220. package/spec/mock-project/dist/src/handlers/PostLogin.js +25 -51
  221. package/spec/mock-project/dist/src/handlers/PostLogin.js.map +1 -0
  222. package/spec/mock-project/dist/src/handlers/PostLogout.js +24 -50
  223. package/spec/mock-project/dist/src/handlers/PostLogout.js.map +1 -0
  224. package/spec/mock-project/dist/src/handlers/PutCar.js +24 -50
  225. package/spec/mock-project/dist/src/handlers/PutCar.js.map +1 -0
  226. package/spec/mock-project/dist/src/handlers/StreamWriterFactory.js +83 -0
  227. package/spec/mock-project/dist/src/index.js +52 -76
  228. package/spec/mock-project/dist/src/index.js.map +1 -0
  229. package/spec/mock-project/dist/src/mock-data-generator.js +9 -0
  230. package/spec/mock-project/dist/src/repos/CarRepo.js +12 -24
  231. package/spec/mock-project/dist/src/repos/CarRepo.js.map +1 -0
  232. package/spec/mock-project/dist/src/schemas/Car.js +3 -1
  233. package/spec/mock-project/dist/src/schemas/Car.js.map +1 -0
  234. package/spec/mock-project/dist/src/schemas/DefaultExportSchema.js +3 -1
  235. package/spec/mock-project/dist/src/schemas/DefaultExportSchema.js.map +1 -0
  236. package/spec/mock-project/dist/src/schemas/FileWithTwoSchemas.js +3 -1
  237. package/spec/mock-project/dist/src/schemas/FileWithTwoSchemas.js.map +1 -0
  238. package/spec/mock-project/dist/src/utils.js +290 -0
  239. package/spec/mock-project/tsconfig.json +6 -1
  240. package/spec/schema-generation-nested-objects.spec.ts +97 -0
  241. package/spec/testHelpers.ts +49 -0
  242. package/spec/utils.caseConversion.spec.ts +78 -0
  243. package/spec/utils.spec.ts +13 -13
  244. package/src/DependencyTracker.ts +166 -0
  245. package/src/FlinkApp.ts +919 -155
  246. package/src/FlinkContext.ts +43 -0
  247. package/src/FlinkErrors.ts +32 -12
  248. package/src/FlinkHttpHandler.ts +246 -28
  249. package/src/FlinkJob.ts +11 -0
  250. package/src/FlinkLog.ts +119 -12
  251. package/src/FlinkLogFactory.ts +699 -0
  252. package/src/FlinkRepo.ts +10 -3
  253. package/src/FlinkRequestContext.ts +95 -0
  254. package/src/FlinkResponse.ts +6 -0
  255. package/src/FlinkService.ts +49 -0
  256. package/src/LeaderElection.ts +203 -0
  257. package/src/SchemaCache.ts +232 -0
  258. package/src/TypeScriptCompiler.ts +1347 -610
  259. package/src/TypeScriptUtils.ts +5 -0
  260. package/src/ai/AgentRunner.ts +646 -0
  261. package/src/ai/ConversationAgent.ts +413 -0
  262. package/src/ai/FlinkAgent.ts +1069 -0
  263. package/src/ai/FlinkTool.ts +165 -0
  264. package/src/ai/InMemoryConversationAgent.ts +149 -0
  265. package/src/ai/LLMAdapter.ts +126 -0
  266. package/src/ai/ToolExecutor.ts +485 -0
  267. package/src/ai/agentInstructions.ts +245 -0
  268. package/src/ai/index.ts +8 -0
  269. package/src/ai/instructionFileLoader.ts +156 -0
  270. package/src/auth/FlinkAuthPlugin.ts +2 -1
  271. package/src/handlers/StreamWriterFactory.ts +84 -0
  272. package/src/index.ts +14 -0
  273. package/src/loadPluginSchemas.ts +141 -0
  274. package/src/schema-extraction/TypeScriptSourceParser.ts +1058 -0
  275. package/src/schema-extraction/TypeScriptTokenizer.ts +205 -0
  276. package/src/schema-extraction/index.ts +2 -0
  277. package/src/schema-extraction/types.ts +34 -0
  278. package/src/utils/loadFlinkConfig.ts +89 -0
  279. package/src/utils.ts +52 -0
  280. package/tsconfig.json +6 -1
@@ -0,0 +1,1069 @@
1
+ import { FlinkContext } from "../FlinkContext";
2
+ import { forbidden } from "../FlinkErrors";
3
+ import { FlinkLogFactory } from "../FlinkLogFactory";
4
+ import { getRequestContext, getRequestUser } from "../FlinkRequestContext";
5
+ import { AgentRunner } from "./AgentRunner";
6
+ import { FlinkToolFile, FlinkToolProps } from "./FlinkTool";
7
+ import { resolveInstructionsReturn, type InstructionsReturn } from "./instructionFileLoader";
8
+ export type { InstructionsReturn } from "./instructionFileLoader";
9
+ import { LLMContentBlock, LLMMessage } from "./LLMAdapter";
10
+ import { ToolExecutor } from "./ToolExecutor";
11
+
12
+ const logger = FlinkLogFactory.createLogger("flink.ai.flink-agent");
13
+ const instructionsLog = FlinkLogFactory.createLogger("flink.ai.instructions");
14
+
15
+ /**
16
+ * Callback function for dynamic instruction generation
17
+ * Receives agent context and execution context to build custom instructions
18
+ *
19
+ * @param ctx - FlinkContext with access to repos, plugins, etc.
20
+ * @param agentContext - AgentExecuteContext with user, conversationId, metadata, etc.
21
+ * @returns Instructions string (can be async)
22
+ *
23
+ * @example
24
+ * const instructions = async (ctx, agentContext) => {
25
+ * const profile = await ctx.repos.userRepo.getById(agentContext.user.id);
26
+ * return `You are a support agent. Customer tier: ${profile.tier}`;
27
+ * };
28
+ */
29
+ export type InstructionsCallback<Ctx extends FlinkContext> = (ctx: Ctx, agentContext: AgentExecuteContext) => Promise<string> | string;
30
+
31
+ /**
32
+ * Agent instructions can be either a static string or a dynamic callback
33
+ */
34
+ export type AgentInstructions<Ctx extends FlinkContext> = string | InstructionsCallback<Ctx>;
35
+
36
+ /**
37
+ * Callback to determine if history compaction is needed
38
+ * Called before each LLM call to check if messages should be compacted
39
+ *
40
+ * @param messages - Current conversation messages
41
+ * @param step - Current step number (1-indexed)
42
+ * @returns true if compaction should be performed
43
+ *
44
+ * @example Check message count
45
+ * shouldCompact: (messages) => messages.length > 20
46
+ *
47
+ * @example Check total size in bytes
48
+ * shouldCompact: (messages) => {
49
+ * const totalSize = JSON.stringify(messages).length;
50
+ * return totalSize > 100_000; // ~100KB
51
+ * }
52
+ *
53
+ * @example Estimate token count (rough: 1 token ≈ 4 chars)
54
+ * shouldCompact: (messages) => {
55
+ * const totalChars = messages.reduce((sum, m) => {
56
+ * if (typeof m.content === 'string') return sum + m.content.length;
57
+ * if (Array.isArray(m.content)) {
58
+ * return sum + m.content.reduce((s, block) => {
59
+ * if (block.type === 'text') return s + block.text.length;
60
+ * if (block.type === 'tool_result') return s + JSON.stringify(block.content).length;
61
+ * return s;
62
+ * }, 0);
63
+ * }
64
+ * return sum;
65
+ * }, 0);
66
+ * return totalChars > 60_000; // ~15k tokens
67
+ * }
68
+ */
69
+ export type ShouldCompactCallback = (messages: LLMMessage[], step: number) => boolean | Promise<boolean>;
70
+
71
+ /**
72
+ * Callback to compact conversation history
73
+ * Called when shouldCompact returns true
74
+ *
75
+ * @param messages - Current conversation messages to compact
76
+ * @param step - Current step number (1-indexed)
77
+ * @returns Compacted messages array (must not be empty)
78
+ *
79
+ * @example Sliding window (keep last N messages)
80
+ * compactHistory: (messages) => messages.slice(-10)
81
+ *
82
+ * @example Keep first message + last N (preserve initial context)
83
+ * compactHistory: (messages) => [messages[0], ...messages.slice(-8)]
84
+ *
85
+ * @example LLM-based summarization
86
+ * compactHistory: async (messages, step) => {
87
+ * const oldMessages = messages.slice(0, -5);
88
+ * const recentMessages = messages.slice(-5);
89
+ * const summary = await this.summarize(oldMessages);
90
+ * return [
91
+ * { role: "user", content: `[Summary: ${summary}]` },
92
+ * ...recentMessages
93
+ * ];
94
+ * }
95
+ */
96
+ export type CompactHistoryCallback = (messages: LLMMessage[], step: number) => Promise<LLMMessage[]> | LLMMessage[];
97
+
98
+ export interface FlinkAgentProps<Ctx extends FlinkContext> {
99
+ id: string;
100
+ description: string;
101
+
102
+ /**
103
+ * Instructions that define the agent's behavior and personality.
104
+ * Can be a static string or a callback function that generates dynamic instructions.
105
+ *
106
+ * @example Static instructions
107
+ * instructions: "You are a helpful customer support agent."
108
+ *
109
+ * @example Dynamic instructions
110
+ * instructions: async (ctx, agentContext) => {
111
+ * const user = await ctx.repos.userRepo.getById(agentContext.user.id);
112
+ * return `You are a support agent. Customer: ${user.name}, Tier: ${user.tier}`;
113
+ * }
114
+ */
115
+ instructions: string | InstructionsCallback<Ctx>;
116
+
117
+ tools?: Array<string | FlinkToolFile | FlinkToolProps>; // Tool ids, tool file references, or tool props (validated at startup) - optional, defaults to []
118
+ model?: {
119
+ /**
120
+ * ID of the LLM adapter to use (must be registered in FlinkOptions.ai.llmAdapters)
121
+ * Examples: "anthropic", "openai", "anthropic-eu", "gpt4", etc.
122
+ * Defaults to "default" if not specified
123
+ */
124
+ adapterId?: string;
125
+ maxTokens?: number;
126
+ temperature?: number;
127
+ };
128
+ limits?: {
129
+ maxSteps?: number; // Default: 10
130
+ timeoutMs?: number; // Phase 2: Not yet implemented
131
+ };
132
+ permissions?: string | string[] | ((user?: any) => boolean);
133
+
134
+ /**
135
+ * Callback to determine if history compaction is needed
136
+ * Called before each LLM call in the agentic loop
137
+ *
138
+ * When shouldCompact returns true, the framework calls compactHistory
139
+ * to reduce the message array size before the next LLM call
140
+ *
141
+ * If not provided, no compaction occurs (backward compatible)
142
+ */
143
+ shouldCompact?: ShouldCompactCallback;
144
+
145
+ /**
146
+ * Callback to compact conversation history
147
+ * Called when shouldCompact returns true
148
+ *
149
+ * Must return a non-empty array of messages
150
+ * If not provided but shouldCompact triggers, uses default strategy (keep last 10 messages)
151
+ */
152
+ compactHistory?: CompactHistoryCallback;
153
+
154
+ // Lifecycle hooks (passed from agent instance)
155
+ beforeRun?: (input: AgentExecuteInput<any>, context: AgentExecuteContext) => void | Promise<void>;
156
+ onStep?: (context: AgentStepContext) => void | Promise<void>;
157
+ afterRun?: (result: AgentExecuteResult, context: AgentFinishContext) => void | Promise<void>;
158
+ }
159
+
160
+ export type FlinkAgentFile<Ctx extends FlinkContext, ConversationCtx = any> = {
161
+ default?: new () => FlinkAgent<Ctx, ConversationCtx>; // Class extending FlinkAgent
162
+ __file?: string; // Set by compiler
163
+ };
164
+
165
+ /**
166
+ * Base class for Flink agents (similar to FlinkRepo pattern)
167
+ *
168
+ * Agents extend this class and define their configuration as properties.
169
+ * Auto-registered by scanning src/agents/ directory.
170
+ *
171
+ * Tool references are validated at startup to ensure all referenced tools exist.
172
+ *
173
+ * Agents define their own domain-specific entry points that call `this.execute()`.
174
+ * This provides better type safety and developer experience than a generic `run()` method.
175
+ *
176
+ * ## Lifecycle Hooks
177
+ *
178
+ * Agents support lifecycle hooks for advanced orchestration:
179
+ * - `beforeRun`: Load conversation history, prepare context
180
+ * - `onStep`: Save state after each LLM turn
181
+ * - `afterRun`: Persist final conversation state
182
+ *
183
+ * ## Context Compaction
184
+ *
185
+ * Long-running agents can accumulate large message histories. Use context compaction
186
+ * to automatically manage history size during execution:
187
+ *
188
+ * ```typescript
189
+ * export default class MyAgent extends FlinkAgent<AppCtx> {
190
+ * id = "my-agent";
191
+ * description = "Agent with automatic context management";
192
+ * instructions = "You are a helpful assistant...";
193
+ *
194
+ * // Compact when history exceeds 20 messages
195
+ * shouldCompact = (messages) => messages.length > 20;
196
+ *
197
+ * // Keep only last 10 messages
198
+ * compactHistory = (messages) => messages.slice(-10);
199
+ * }
200
+ * ```
201
+ *
202
+ * Compaction happens before each LLM call in the agentic loop, ensuring consistent
203
+ * token usage and preventing context window overflow.
204
+ *
205
+ * Example:
206
+ * ```typescript
207
+ * import { Tool as GetCarsTool } from "./tools/GetCarsTool";
208
+ * import * as UpdateCarTool from "./tools/UpdateCarTool";
209
+ *
210
+ * export default class CarAgent extends FlinkAgent<AppCtx> {
211
+ * id = "car-agent"; // Optional: defaults to kebab-case class name "car-agent"
212
+ * description = "Expert in car models";
213
+ * instructions = "You are a car expert...";
214
+ * tools = [
215
+ * "get-cars-tool", // String ID reference
216
+ * GetCarsTool, // Direct FlinkToolProps import
217
+ * UpdateCarTool, // Tool file reference
218
+ * ];
219
+ *
220
+ * // Domain-specific entry points with proper types
221
+ * async searchByBrand(brand: string) {
222
+ * const response = this.run({
223
+ * message: `Find all ${brand} cars`
224
+ * });
225
+ * return await response.result;
226
+ * }
227
+ *
228
+ * async compareModels(model1: string, model2: string) {
229
+ * const response = this.run({
230
+ * message: `Compare ${model1} and ${model2}`
231
+ * });
232
+ * return await response.result;
233
+ * }
234
+ *
235
+ * // Lifecycle hook: Load conversation history
236
+ * protected async beforeRun(input, context) {
237
+ * if (context.conversationId) {
238
+ * const conv = await this.ctx.repos.conversationRepo.getById(context.conversationId);
239
+ * input.history = conv.messages;
240
+ * }
241
+ * }
242
+ * }
243
+ * ```
244
+ */
245
+ export abstract class FlinkAgent<Ctx extends FlinkContext, ConversationCtx = any> {
246
+ ctx!: Ctx;
247
+ private runner?: AgentRunner;
248
+ private _boundUser?: any; // User bound via withUser()
249
+ private _boundUserPermissions?: string[]; // User permissions bound via withPermissions()
250
+ private _boundConversationContext?: ConversationCtx; // Conversation context bound via withConversationContext()
251
+ private _llmAdapters?: Map<string, any>;
252
+ private _tools?: { [x: string]: ToolExecutor<Ctx> };
253
+ private _observer?: AgentObserver;
254
+
255
+ // Abstract properties (must be defined by subclass)
256
+ abstract id: string;
257
+ abstract description: string;
258
+
259
+ /**
260
+ * Define the agent's instructions. Override this method in your agent subclass.
261
+ *
262
+ * `ctx` is automatically typed as your app's context — no annotation needed.
263
+ *
264
+ * Supported return values:
265
+ * - `string` — Plain text used as-is
266
+ * - `string` ending with a known text extension (`.md`, `.txt`, `.yaml`, `.yml`, `.xml`, …) — Auto-loaded from disk (project-root-relative)
267
+ * - `{ file, params? }` — Explicitly load a file (any extension) with optional Handlebars template params
268
+ *
269
+ * @example Plain text
270
+ * instructions() {
271
+ * return "You are a helpful car assistant.";
272
+ * }
273
+ *
274
+ * @example Dynamic instructions using ctx and agentContext
275
+ * async instructions(ctx, agentContext) {
276
+ * const user = await ctx.repos.userRepo.getById(agentContext.user?.id);
277
+ * return `You are a support agent for ${user.name}.`;
278
+ * }
279
+ *
280
+ * @example Auto-load file by extension (.md, .txt, .yaml, .yml, .xml, … — path relative to project root)
281
+ * instructions() {
282
+ * return "src/agents/instructions/car-agent.md";
283
+ * }
284
+ *
285
+ * @example File with template params
286
+ * async instructions(ctx, agentContext) {
287
+ * return {
288
+ * file: "src/agents/instructions/support.md",
289
+ * params: {
290
+ * customerTier: agentContext.user?.tier || "standard",
291
+ * isBusinessHours: new Date().getHours() >= 9,
292
+ * },
293
+ * };
294
+ * }
295
+ *
296
+ * @example Agent-file-relative path (use agentInstructions helper)
297
+ * instructions(ctx, agentContext) {
298
+ * return agentInstructions("./instructions/support.md", { date: new Date() })(ctx, agentContext);
299
+ * }
300
+ */
301
+ abstract instructions(ctx: Ctx, agentContext: AgentExecuteContext): Promise<InstructionsReturn> | InstructionsReturn;
302
+
303
+ // Optional properties
304
+ tools?: Array<string | FlinkToolFile | FlinkToolProps>; // Tool ids, tool file references, or tool props (defaults to empty array)
305
+ model?: {
306
+ adapterId?: string;
307
+ maxTokens?: number;
308
+ temperature?: number;
309
+ };
310
+ limits?: { maxSteps?: number; timeoutMs?: number };
311
+ permissions?: string | string[] | ((user?: any) => boolean);
312
+ protected shouldCompact?: ShouldCompactCallback;
313
+ protected compactHistory?: CompactHistoryCallback;
314
+
315
+ /**
316
+ * Internal initialization called by FlinkApp
317
+ * @internal
318
+ */
319
+ __init(llmAdapters: Map<string, any>, tools: { [x: string]: ToolExecutor<Ctx> }, observer?: AgentObserver): void {
320
+ this._llmAdapters = llmAdapters;
321
+ this._tools = tools;
322
+ this._observer = observer;
323
+ }
324
+
325
+ /**
326
+ * Bind a user to this agent for permission checks
327
+ *
328
+ * This creates a new agent instance with the user bound, allowing clean API:
329
+ * ```typescript
330
+ * const result = await ctx.agents.carAgent
331
+ * .withUser(req.user)
332
+ * .searchByBrand("Volvo");
333
+ * ```
334
+ *
335
+ * The bound user is used for:
336
+ * - Agent-level permission checks
337
+ * - Tool filtering (only allowed tools shown to LLM)
338
+ * - Tool-level permission checks
339
+ */
340
+ withUser(user: any): this {
341
+ const bound = Object.create(Object.getPrototypeOf(this));
342
+ Object.assign(bound, this);
343
+ bound._boundUser = user;
344
+ bound.runner = undefined; // Clear runner cache to use new user
345
+ // Explicitly ensure ctx and internal properties are copied (in case they're not enumerable)
346
+ if (this.ctx) {
347
+ bound.ctx = this.ctx;
348
+ }
349
+ if (this._llmAdapters) {
350
+ bound._llmAdapters = this._llmAdapters;
351
+ }
352
+ if (this._tools) {
353
+ bound._tools = this._tools;
354
+ }
355
+ if (this._observer) {
356
+ bound._observer = this._observer;
357
+ }
358
+ if (this._boundUserPermissions !== undefined) {
359
+ bound._boundUserPermissions = this._boundUserPermissions;
360
+ }
361
+ return bound;
362
+ }
363
+
364
+ /**
365
+ * Bind resolved permissions to this agent for permission checks
366
+ *
367
+ * This creates a new agent instance with permissions bound, allowing clean API:
368
+ * ```typescript
369
+ * const result = await ctx.agents.carAgent
370
+ * .withUser(req.user)
371
+ * .withPermissions(req.userPermissions) // Resolved permissions from auth plugin
372
+ * .searchByBrand("Volvo");
373
+ * ```
374
+ *
375
+ * The bound permissions are used for:
376
+ * - Tool filtering (only allowed tools shown to LLM)
377
+ * - Tool-level permission checks
378
+ *
379
+ * Permissions are typically populated by auth plugins during authentication
380
+ * based on roles, dynamic roles, or custom permission resolution.
381
+ */
382
+ withPermissions(userPermissions?: string[]): this {
383
+ const bound = Object.create(Object.getPrototypeOf(this));
384
+ Object.assign(bound, this);
385
+ bound._boundUserPermissions = userPermissions;
386
+ bound.runner = undefined; // Clear runner cache to use new permissions
387
+ // Explicitly ensure ctx and internal properties are copied (in case they're not enumerable)
388
+ if (this.ctx) {
389
+ bound.ctx = this.ctx;
390
+ }
391
+ if (this._llmAdapters) {
392
+ bound._llmAdapters = this._llmAdapters;
393
+ }
394
+ if (this._tools) {
395
+ bound._tools = this._tools;
396
+ }
397
+ if (this._observer) {
398
+ bound._observer = this._observer;
399
+ }
400
+ if (this._boundUser !== undefined) {
401
+ bound._boundUser = this._boundUser;
402
+ }
403
+ return bound;
404
+ }
405
+
406
+ /**
407
+ * Override the LLM adapter for this agent
408
+ *
409
+ * This creates a new agent instance with a different LLM adapter, allowing runtime selection:
410
+ * ```typescript
411
+ * const result = await ctx.agents.carAgent
412
+ * .withUser(req.user)
413
+ * .withLlm("fast") // Use fast LLM instead of default
414
+ * .searchByBrand("Volvo");
415
+ * ```
416
+ *
417
+ * The LLM adapter ID must be registered in FlinkApp's ai.llms configuration.
418
+ *
419
+ * @param adapterId - The ID of the LLM adapter to use (e.g., "default", "fake", "fast", "anthropic")
420
+ * @returns A new agent instance with the specified LLM adapter
421
+ */
422
+ withLlm(adapterId: string): this {
423
+ const bound = Object.create(Object.getPrototypeOf(this));
424
+ Object.assign(bound, this);
425
+ // Override the model configuration with the new adapter ID
426
+ bound.model = {
427
+ ...(this.model || {}),
428
+ adapterId,
429
+ };
430
+ bound.runner = undefined; // Clear runner cache to use new adapter
431
+ // Explicitly ensure ctx and internal properties are copied (in case they're not enumerable)
432
+ if (this.ctx) {
433
+ bound.ctx = this.ctx;
434
+ }
435
+ if (this._llmAdapters) {
436
+ bound._llmAdapters = this._llmAdapters;
437
+ }
438
+ if (this._tools) {
439
+ bound._tools = this._tools;
440
+ }
441
+ if (this._observer) {
442
+ bound._observer = this._observer;
443
+ }
444
+ if (this._boundUser !== undefined) {
445
+ bound._boundUser = this._boundUser;
446
+ }
447
+ if (this._boundUserPermissions !== undefined) {
448
+ bound._boundUserPermissions = this._boundUserPermissions;
449
+ }
450
+ return bound;
451
+ }
452
+
453
+ /**
454
+ * Bind conversation context to this agent for passing to tools
455
+ *
456
+ * This creates a new agent instance with the conversation context bound, allowing clean API:
457
+ * ```typescript
458
+ * const result = await ctx.agents.carAgent
459
+ * .withUser(req.user)
460
+ * .withConversationContext({ sessionId: req.session.id, featureFlags: req.flags })
461
+ * .searchByBrand("Volvo");
462
+ * ```
463
+ *
464
+ * The bound conversation context is used for:
465
+ * - Passing request-scoped data to tools
466
+ * - Sharing state between agent and tools without polluting global context
467
+ * - Propagating to sub-agents automatically
468
+ */
469
+ withConversationContext(conversationContext: ConversationCtx): this {
470
+ const bound = Object.create(Object.getPrototypeOf(this));
471
+ Object.assign(bound, this);
472
+ bound._boundConversationContext = conversationContext;
473
+ bound.runner = undefined; // Clear runner cache
474
+
475
+ // Copy non-enumerable properties
476
+ if (this.ctx) {
477
+ bound.ctx = this.ctx;
478
+ }
479
+ if (this._llmAdapters) {
480
+ bound._llmAdapters = this._llmAdapters;
481
+ }
482
+ if (this._tools) {
483
+ bound._tools = this._tools;
484
+ }
485
+ if (this._observer) {
486
+ bound._observer = this._observer;
487
+ }
488
+ if (this._boundUser !== undefined) {
489
+ bound._boundUser = this._boundUser;
490
+ }
491
+ if (this._boundUserPermissions !== undefined) {
492
+ bound._boundUserPermissions = this._boundUserPermissions;
493
+ }
494
+
495
+ return bound;
496
+ }
497
+
498
+ /**
499
+ * Public execution method for external callers (handlers, sub-agents, etc.)
500
+ *
501
+ * Use this when calling an agent from outside the agent class.
502
+ * For internal use within agent subclasses, use `execute()` instead.
503
+ */
504
+ public run(input: AgentExecuteInput<ConversationCtx>): AgentResponse {
505
+ return this.execute(input);
506
+ }
507
+
508
+ /**
509
+ * Internal execution method - supports both awaiting and streaming
510
+ *
511
+ * Uses lazy generator pattern (similar to Vercel AI SDK) to allow
512
+ * multiple consumption without re-execution.
513
+ *
514
+ * Agents call this method from their run() implementation to execute the AI logic.
515
+ *
516
+ * Examples:
517
+ * const response = this.execute({ message: "Hello" });
518
+ * const result = await response.result; // Await final result
519
+ * for await (const text of response.textStream) { ... } // Stream text
520
+ * for await (const chunk of response.fullStream) { ... } // Stream all events
521
+ */
522
+ protected execute(input: AgentExecuteInput<ConversationCtx>): AgentResponse {
523
+ // Use bound user if not explicitly provided in input, fall back to AsyncLocalStorage request context
524
+ const user = input.user ?? this._boundUser ?? getRequestUser();
525
+ const userPermissions = input.userPermissions ?? this._boundUserPermissions ?? getRequestContext()?.userPermissions;
526
+ const conversationContext = input.conversationContext ?? this._boundConversationContext;
527
+ const executeInput = { ...input, user, userPermissions, conversationContext };
528
+
529
+ // Permission check
530
+ if (this.permissions) {
531
+ this.checkPermissionsSync(user, userPermissions);
532
+ }
533
+
534
+ // Build execution context
535
+ const execContext: AgentExecuteContext = {
536
+ agentId: this.getAgentId(),
537
+ conversationId: executeInput.conversationId,
538
+ user,
539
+ metadata: executeInput.metadata,
540
+ conversationContext: executeInput.conversationContext,
541
+ };
542
+
543
+ const runner = this.getRunner();
544
+ logger.debug(`Running agent ${this.constructor.name}`);
545
+
546
+ // Lazy evaluation - generator only starts when first consumed
547
+ let baseGenerator: AsyncGenerator<StreamChunk> | null = null;
548
+ const buffer: StreamChunk[] = [];
549
+ let done = false;
550
+ let fetchPromise: Promise<void> | null = null; // Prevent concurrent fetches
551
+ let beforeRunCalled = false;
552
+
553
+ const getBaseGenerator = async () => {
554
+ if (!baseGenerator) {
555
+ // Call optional beforeRun hook before starting generator (only once)
556
+ if (!beforeRunCalled && this.beforeRun) {
557
+ beforeRunCalled = true;
558
+ await this.beforeRun(executeInput, execContext);
559
+ }
560
+ baseGenerator = runner.streamGenerator(executeInput);
561
+ }
562
+ return baseGenerator;
563
+ };
564
+
565
+ // Fetch next chunk from base generator (only one consumer at a time)
566
+ const fetchNext = async (): Promise<void> => {
567
+ if (fetchPromise) {
568
+ // Another iterator is already fetching, wait for it
569
+ await fetchPromise;
570
+ return;
571
+ }
572
+
573
+ if (done) {
574
+ return;
575
+ }
576
+
577
+ fetchPromise = (async () => {
578
+ const gen = await getBaseGenerator();
579
+ const { value, done: isDone } = await gen.next();
580
+
581
+ if (isDone) {
582
+ done = true;
583
+ } else {
584
+ buffer.push(value);
585
+ }
586
+ })();
587
+
588
+ await fetchPromise;
589
+ fetchPromise = null;
590
+ };
591
+
592
+ // Create independent iterators that share buffered chunks
593
+ const createIterator = (): AsyncGenerator<StreamChunk> => {
594
+ let index = 0;
595
+
596
+ return (async function* () {
597
+ while (true) {
598
+ // Yield buffered chunks first
599
+ if (index < buffer.length) {
600
+ yield buffer[index++];
601
+ continue;
602
+ }
603
+
604
+ // If already done, exit
605
+ if (done) {
606
+ break;
607
+ }
608
+
609
+ // Fetch next chunk (synchronized)
610
+ await fetchNext();
611
+
612
+ // Check again after fetch
613
+ if (index < buffer.length) {
614
+ yield buffer[index++];
615
+ } else if (done) {
616
+ break;
617
+ }
618
+ }
619
+ })();
620
+ };
621
+
622
+ return {
623
+ result: this.consumeAsResult(createIterator()),
624
+ textStream: this.consumeAsTextStream(createIterator()),
625
+ fullStream: createIterator(),
626
+ };
627
+ }
628
+
629
+ // Optional lifecycle hooks
630
+ /**
631
+ * Called before agent execution starts
632
+ * Use this to load conversation history, prepare context, etc.
633
+ *
634
+ * @example
635
+ * protected async beforeRun(input: AgentExecuteInput, context: AgentExecuteContext) {
636
+ * if (input.conversationId) {
637
+ * const conv = await this.ctx.repos.conversationRepo.getById(input.conversationId);
638
+ * input.history = conv?.messages;
639
+ * }
640
+ * }
641
+ */
642
+ protected beforeRun?(input: AgentExecuteInput<ConversationCtx>, context: AgentExecuteContext): void | Promise<void>;
643
+
644
+ /**
645
+ * Called after each step (LLM call + tool executions)
646
+ * Use this to save intermediate state, emit progress events, etc.
647
+ *
648
+ * @example
649
+ * protected async onStep(context: AgentStepContext) {
650
+ * if (context.conversationId) {
651
+ * await this.saveConversationState(context.conversationId, context.messages);
652
+ * }
653
+ * }
654
+ */
655
+ protected onStep?(context: AgentStepContext): void | Promise<void>;
656
+
657
+ /**
658
+ * Called when agent execution completes
659
+ * This is where you typically save the final conversation state
660
+ *
661
+ * @example
662
+ * protected async afterRun(result: AgentExecuteResult, context: AgentFinishContext) {
663
+ * if (context.conversationId && !context.isSubAgent) {
664
+ * await this.ctx.repos.conversationRepo.save({
665
+ * id: context.conversationId,
666
+ * messages: context.messages,
667
+ * result
668
+ * });
669
+ * }
670
+ * }
671
+ */
672
+ protected afterRun?(result: AgentExecuteResult, context: AgentFinishContext): void | Promise<void>;
673
+
674
+ /**
675
+ * Consume stream as final result (for await pattern)
676
+ */
677
+ private async consumeAsResult(stream: AsyncGenerator<StreamChunk>): Promise<AgentExecuteResult> {
678
+ let result: AgentExecuteResult | undefined;
679
+
680
+ try {
681
+ for await (const chunk of stream) {
682
+ if (chunk.type === "complete") {
683
+ result = chunk.result;
684
+ break; // Exit early once we have the result
685
+ }
686
+ }
687
+ } catch (err) {
688
+ // If stream was already consumed or interrupted, that's okay
689
+ // as long as we got the result
690
+ if (!result) {
691
+ throw err;
692
+ }
693
+ }
694
+
695
+ if (!result) {
696
+ throw new Error("Agent execution did not complete");
697
+ }
698
+
699
+ // Note: afterRun hook is called by AgentRunner now with full context
700
+ return result;
701
+ }
702
+
703
+ /**
704
+ * Consume stream as text-only stream (for simple streaming UX)
705
+ */
706
+ private async *consumeAsTextStream(stream: AsyncGenerator<StreamChunk>): AsyncGenerator<string> {
707
+ for await (const chunk of stream) {
708
+ if (chunk.type === "text_delta") {
709
+ yield chunk.delta;
710
+ }
711
+ }
712
+ }
713
+
714
+ private getRunner(): AgentRunner {
715
+ if (!this.runner) {
716
+ if (!this._llmAdapters) {
717
+ throw new Error("Agent not initialized - __init() must be called by FlinkApp");
718
+ }
719
+
720
+ // Get tools map and LLM adapters from internal properties
721
+ const toolsMap = this.resolveTools();
722
+ const llmAdapters = this._llmAdapters;
723
+
724
+ this.runner = new AgentRunner(
725
+ this.toAgentProps(),
726
+ toolsMap,
727
+ llmAdapters,
728
+ this.getAgentId(),
729
+ this.ctx, // Pass ctx to runner so callbacks can access it
730
+ this._observer
731
+ );
732
+ }
733
+ return this.runner;
734
+ }
735
+
736
+ /**
737
+ * Get agent id - uses explicit id property or falls back to kebab-case class name
738
+ *
739
+ * Examples:
740
+ * - CarAgent → car-agent
741
+ * - APIAgent → api-agent
742
+ * - HTMLParserAgent → html-parser-agent
743
+ */
744
+ private getAgentId(): string {
745
+ return this.id;
746
+ }
747
+
748
+ private toAgentProps(): FlinkAgentProps<Ctx> {
749
+ return {
750
+ id: this.getAgentId(),
751
+ description: this.description,
752
+ instructions: async (ctx, agentContext) => {
753
+ const resolved = await resolveInstructionsReturn(await Promise.resolve(this.instructions(ctx as Ctx, agentContext)), ctx, agentContext);
754
+ instructionsLog.debug(`[${this.getAgentId()}] Resolved instructions:\n${resolved}`);
755
+ return resolved;
756
+ },
757
+ tools: this.tools,
758
+ model: this.model,
759
+ limits: this.limits,
760
+ permissions: this.permissions,
761
+ // Pass context compaction callbacks
762
+ shouldCompact: this.shouldCompact?.bind(this),
763
+ compactHistory: this.compactHistory?.bind(this),
764
+ // Pass lifecycle hooks
765
+ beforeRun: this.beforeRun?.bind(this),
766
+ onStep: this.onStep?.bind(this),
767
+ afterRun: this.afterRun?.bind(this),
768
+ };
769
+ }
770
+
771
+ private resolveTools(): Map<string, ToolExecutor<Ctx>> {
772
+ const toolsMap = new Map<string, ToolExecutor<Ctx>>();
773
+
774
+ if (!this._tools) {
775
+ throw new Error("Agent not initialized - __init() must be called by FlinkApp");
776
+ }
777
+
778
+ const getTool = (name: string) => this._tools![name];
779
+
780
+ // Resolve tool names/references to tool executors
781
+ if (this.tools) {
782
+ for (const toolRef of this.tools) {
783
+ // Handle string IDs, tool file references, and tool props
784
+ let toolId: string;
785
+ if (typeof toolRef === "string") {
786
+ toolId = toolRef;
787
+ } else if ("Tool" in toolRef) {
788
+ // FlinkToolFile - extract ID from Tool property
789
+ toolId = toolRef.Tool.id;
790
+ } else {
791
+ // FlinkToolProps - extract ID directly
792
+ toolId = toolRef.id;
793
+ }
794
+
795
+ const tool = getTool(toolId);
796
+ if (!tool) {
797
+ throw new Error(`Tool ${toolId} not found in context`);
798
+ }
799
+ toolsMap.set(toolId, tool);
800
+ }
801
+ }
802
+
803
+ return toolsMap;
804
+ }
805
+
806
+ private checkPermissionsSync(user?: any, userPermissions?: string[]): void {
807
+ const perms = this.permissions;
808
+ if (!perms) return;
809
+
810
+ if (typeof perms === "function") {
811
+ const hasPermission = perms(user);
812
+ if (!hasPermission) {
813
+ throw forbidden(`Permission denied for agent ${this.getAgentId()}`);
814
+ }
815
+ return;
816
+ }
817
+
818
+ // Get effective permissions (prefer explicit userPermissions)
819
+ const effectivePerms = userPermissions || user?.permissions || [];
820
+
821
+ // If no user and no explicit permissions, deny access
822
+ if (!user && !userPermissions) {
823
+ throw forbidden(`Permission denied for agent ${this.getAgentId()}`);
824
+ }
825
+
826
+ const requiredPerms = Array.isArray(perms) ? perms : [perms];
827
+
828
+ if (!requiredPerms.every((p) => effectivePerms.includes(p))) {
829
+ throw forbidden(`Permission denied for agent ${this.getAgentId()}`);
830
+ }
831
+ }
832
+ }
833
+
834
+ /**
835
+ * Execution context shared across agent lifecycle hooks
836
+ */
837
+ export interface AgentExecuteContext {
838
+ agentId: string;
839
+ conversationId?: string;
840
+ user?: any;
841
+ metadata?: Record<string, any>; // Arbitrary metadata from input
842
+ conversationContext?: any; // Transient conversation-scoped data passed from agent to tools
843
+ }
844
+
845
+ /**
846
+ * Context for onStep hook - includes current conversation state
847
+ */
848
+ export interface AgentStepContext extends AgentExecuteContext {
849
+ step: number;
850
+ maxSteps: number;
851
+ messages: LLMMessage[]; // Current conversation state (read-only)
852
+ }
853
+
854
+ /**
855
+ * Context for afterRun hook - includes final conversation and result
856
+ */
857
+ export interface AgentFinishContext extends AgentExecuteContext {
858
+ messages: LLMMessage[]; // Full conversation including final turn (read-only)
859
+ result: AgentExecuteResult;
860
+ }
861
+
862
+ /**
863
+ * Message structure for agent conversations
864
+ * Supports both simple strings and structured message arrays
865
+ */
866
+ export type Message =
867
+ | { role: "user"; content: string }
868
+ | { role: "assistant"; content: string; toolCalls?: ToolCall[] }
869
+ | { role: "tool"; toolCallId: string; toolName: string; result: string };
870
+
871
+ export interface ToolCall {
872
+ id: string;
873
+ name: string;
874
+ input: any;
875
+ }
876
+
877
+ export interface AgentExecuteInput<ConversationCtx = any> {
878
+ message: string | LLMContentBlock[] | Message[]; // string, multimodal content blocks, or structured messages
879
+ user?: any;
880
+ userPermissions?: string[]; // Resolved permissions from auth plugin
881
+
882
+ /**
883
+ * Optional ID for tracking multi-turn conversations
884
+ * Used in lifecycle hooks (beforeRun/onStep/afterRun) to load/save conversation state
885
+ * @example "conv_abc123" - caller-provided ID to persist conversation history
886
+ */
887
+ conversationId?: string;
888
+
889
+ /**
890
+ * Transient, conversation-scoped data that flows from agent to tools
891
+ * Examples: session IDs, feature flags, request tracing, tenant config
892
+ * Typed by agent's ConversationCtx parameter
893
+ */
894
+ conversationContext?: ConversationCtx;
895
+
896
+ /**
897
+ * Previous conversation messages for context, in chronological order (oldest first).
898
+ * The new `message` will be appended after these history messages.
899
+ * Agent can load this in beforeRun hook based on conversationId.
900
+ */
901
+ history?: Message[];
902
+
903
+ /**
904
+ * Provider-specific metadata for optimizations like server-side persistence
905
+ *
906
+ * Examples:
907
+ * - OpenAI Responses API: `{ openai: { previousResponseId: "resp_123" } }`
908
+ * - Anthropic: `{ anthropic: { ... } }`
909
+ *
910
+ * When provided, adapters may use this instead of full history for better performance
911
+ */
912
+ providerMetadata?: Record<string, any>;
913
+
914
+ /**
915
+ * Arbitrary metadata that flows through the execution
916
+ * Available in all callbacks
917
+ */
918
+ metadata?: Record<string, any>;
919
+
920
+ options?: {
921
+ maxSteps?: number;
922
+ timeoutMs?: number;
923
+ };
924
+ }
925
+
926
+ export interface AgentExecuteResult {
927
+ /**
928
+ * Framework-generated unique ID for this agent execution.
929
+ * Matches the `runId` emitted in AgentObserver events, enabling apps to
930
+ * correlate persisted results with observer traces.
931
+ */
932
+ runId: string;
933
+ message: string; // Final AI response
934
+ toolCalls: Array<{
935
+ name: string;
936
+ input: any;
937
+ output: any;
938
+ error?: string;
939
+ }>;
940
+ stepsUsed: number;
941
+ usage?: import("./LLMAdapter").LLMUsage;
942
+ stoppedEarly: boolean; // True if max steps reached
943
+
944
+ /**
945
+ * Provider-specific metadata for conversation continuation
946
+ *
947
+ * Examples:
948
+ * - OpenAI Responses API: `{ openai: { responseId: "resp_123" } }`
949
+ * - Anthropic: `{ anthropic: { ... } }`
950
+ *
951
+ * Use this for next turn's `providerMetadata` input for optimized server-side history
952
+ */
953
+ providerMetadata?: Record<string, any>;
954
+ }
955
+
956
+ /**
957
+ * Stream chunk types for different streaming events
958
+ *
959
+ * Phase 1: Only "complete" event is emitted
960
+ * Phase 2: Will emit real-time "text_delta", "tool_call_start", and "tool_call_result" events
961
+ */
962
+ export type StreamChunk =
963
+ | { type: "text_delta"; delta: string } // Phase 2: Stream text as it's generated
964
+ | { type: "tool_call_start"; toolCall: ToolCall } // Phase 2: Tool execution started
965
+ | {
966
+ type: "tool_call_result"; // Phase 2: Tool execution completed
967
+ toolCall: ToolCall;
968
+ output: any;
969
+ error?: string;
970
+ }
971
+ | { type: "complete"; result: AgentExecuteResult }; // Phase 1: Final result only
972
+
973
+ /**
974
+ * Unified response that supports both awaiting and streaming
975
+ */
976
+ export interface AgentResponse {
977
+ result: Promise<AgentExecuteResult>; // Await for final result
978
+ textStream: AsyncGenerator<string>; // Stream only text deltas
979
+ fullStream: AsyncGenerator<StreamChunk>; // Stream all events
980
+ }
981
+
982
+ /**
983
+ * Event fired once per agent execution, before the first LLM call.
984
+ * Contains the resolved system instructions and the initial message array
985
+ * (post-history, pre-compaction, pre-tool-filtering).
986
+ */
987
+ export interface AgentObserverRunEvent {
988
+ runId: string;
989
+ agentId: string;
990
+ instructions: string;
991
+ input: AgentExecuteInput;
992
+ messages: LLMMessage[];
993
+ tools: string[];
994
+ model: { adapterId?: string; maxTokens?: number; temperature?: number };
995
+ context: AgentExecuteContext;
996
+ }
997
+
998
+ /**
999
+ * Event fired immediately before each LLM call in the agentic loop.
1000
+ * Reflects the messages and tools actually sent to the model after
1001
+ * compaction and per-step permission filtering.
1002
+ */
1003
+ export interface AgentObserverLlmCallEvent {
1004
+ runId: string;
1005
+ agentId: string;
1006
+ step: number;
1007
+ maxSteps: number;
1008
+ instructions: string;
1009
+ messages: LLMMessage[];
1010
+ tools: string[];
1011
+ model: { adapterId?: string; maxTokens?: number; temperature?: number };
1012
+ context: AgentExecuteContext;
1013
+ }
1014
+
1015
+ /**
1016
+ * Event fired after each step (LLM call + tool executions) completes.
1017
+ * `toolCalls` contains only the tool calls executed during this step.
1018
+ */
1019
+ export interface AgentObserverStepEvent {
1020
+ runId: string;
1021
+ agentId: string;
1022
+ step: number;
1023
+ maxSteps: number;
1024
+ messages: LLMMessage[];
1025
+ assistantText?: string;
1026
+ toolCalls: AgentExecuteResult["toolCalls"];
1027
+ usage?: import("./LLMAdapter").LLMUsage;
1028
+ context: AgentExecuteContext;
1029
+ }
1030
+
1031
+ /**
1032
+ * Event fired when an agent execution completes (successfully or with error).
1033
+ * On error, `result` contains whatever was accumulated before the throw.
1034
+ */
1035
+ export interface AgentObserverFinishEvent {
1036
+ runId: string;
1037
+ agentId: string;
1038
+ result: AgentExecuteResult;
1039
+ messages: LLMMessage[];
1040
+ durationMs: number;
1041
+ error?: string;
1042
+ context: AgentExecuteContext;
1043
+ }
1044
+
1045
+ /**
1046
+ * Global agent observer for app-level tracing, APM, cost accounting, dev tools, etc.
1047
+ *
1048
+ * Register once on `FlinkOptions.ai.observer`; fires for every agent execution in the app.
1049
+ *
1050
+ * Observer callbacks are invoked fire-and-forget: they may return a Promise but the
1051
+ * framework does not await them, and any thrown/rejected errors are caught and logged
1052
+ * without affecting agent execution. Observers are read-only — they must not mutate
1053
+ * inputs, messages, or results. Use the per-agent `beforeRun`/`onStep`/`afterRun` hooks
1054
+ * for business logic that needs to block or mutate.
1055
+ *
1056
+ * Typical use cases:
1057
+ * - Persisted traces for a dev tools page (correlate via `context.metadata`)
1058
+ * - OpenTelemetry / Sentry integration
1059
+ * - Cost accounting and token-usage dashboards
1060
+ *
1061
+ * All events share a stable `runId` per execution so persisted records can be joined
1062
+ * across events. The same `runId` is returned on `AgentExecuteResult.runId`.
1063
+ */
1064
+ export interface AgentObserver {
1065
+ onRun?(event: AgentObserverRunEvent): void | Promise<void>;
1066
+ onLlmCall?(event: AgentObserverLlmCallEvent): void | Promise<void>;
1067
+ onStep?(event: AgentObserverStepEvent): void | Promise<void>;
1068
+ onFinish?(event: AgentObserverFinishEvent): void | Promise<void>;
1069
+ }