@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,1062 @@
1
+ import { z } from "zod";
2
+ import { AgentRunner } from "../src/ai/AgentRunner";
3
+ import { FlinkAgentProps } from "../src/ai/FlinkAgent";
4
+ import { ToolExecutor } from "../src/ai/ToolExecutor";
5
+ import { FlinkContext } from "../src/FlinkContext";
6
+ import { FlinkToolProps } from "../src/ai/FlinkTool";
7
+ import { LLMAdapter } from "../src/ai/LLMAdapter";
8
+ import { createStreamingMock } from "./testHelpers";
9
+
10
+ describe("AgentRunner", () => {
11
+ let mockCtx: FlinkContext;
12
+ let mockLLMAdapter: LLMAdapter;
13
+
14
+ beforeEach(() => {
15
+ mockCtx = {
16
+ repos: {},
17
+ plugins: {},
18
+ };
19
+
20
+ // Mock LLM Adapter with default response
21
+ mockLLMAdapter = createStreamingMock([
22
+ {
23
+ textContent: "Test response",
24
+ toolCalls: [],
25
+ usage: { inputTokens: 10, outputTokens: 20 },
26
+ stopReason: "end_turn" as const,
27
+ },
28
+ ]);
29
+ });
30
+
31
+ describe("Basic execution", () => {
32
+ it("should execute simple agent without tools", async () => {
33
+ const agentProps: FlinkAgentProps<typeof mockCtx> = {
34
+ id: "test_agent",
35
+ description: "Test agent",
36
+ instructions: "You are a helpful assistant",
37
+ tools: [],
38
+ };
39
+
40
+ const toolsMap = new Map();
41
+ const llmAdapters = new Map();
42
+ llmAdapters.set("default", mockLLMAdapter);
43
+ const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
44
+
45
+ const generator = runner.streamGenerator({ message: "Hello" });
46
+ const chunks: any[] = [];
47
+
48
+ for await (const chunk of generator) {
49
+ chunks.push(chunk);
50
+ }
51
+
52
+ // Should have complete event
53
+ expect(chunks.length).toBeGreaterThan(0);
54
+ const completeChunk = chunks.find((c) => c.type === "complete");
55
+ expect(completeChunk).toBeDefined();
56
+ expect(completeChunk.result.message).toBe("Test response");
57
+ expect(completeChunk.result.stepsUsed).toBeGreaterThan(0);
58
+ expect(completeChunk.result.toolCalls).toEqual([]);
59
+ });
60
+
61
+ it("should track token usage", async () => {
62
+ const agentProps: FlinkAgentProps<typeof mockCtx> = {
63
+ id: "test_agent",
64
+ description: "Test agent",
65
+ instructions: "You are a helpful assistant",
66
+ tools: [],
67
+ };
68
+
69
+ const toolsMap = new Map();
70
+ const llmAdapters = new Map();
71
+ llmAdapters.set("default", mockLLMAdapter);
72
+ const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
73
+
74
+ const generator = runner.streamGenerator({ message: "Hello" });
75
+
76
+ for await (const chunk of generator) {
77
+ if (chunk.type === "complete") {
78
+ expect(chunk.result.usage).toBeDefined();
79
+ expect(chunk.result.usage?.inputTokens).toBeGreaterThan(0);
80
+ expect(chunk.result.usage?.outputTokens).toBeGreaterThan(0);
81
+ }
82
+ }
83
+ });
84
+ });
85
+
86
+ describe("Tool calling", () => {
87
+ it("should execute tool calls", async () => {
88
+ // Create mock tool
89
+ const toolProps: FlinkToolProps = {
90
+ id: "get_weather",
91
+ description: "Get weather",
92
+ inputSchema: z.object({ city: z.string() }),
93
+ };
94
+
95
+ const toolFn = jasmine.createSpy("toolFn").and.returnValue(
96
+ Promise.resolve({
97
+ success: true,
98
+ data: { temperature: 22, conditions: "sunny" },
99
+ })
100
+ );
101
+
102
+ const toolExecutor = new ToolExecutor(toolProps, toolFn as any, mockCtx);
103
+ const toolsMap = new Map([["get_weather", toolExecutor]]);
104
+
105
+ // Mock LLM adapter response with tool call
106
+ const weatherMockAdapter = createStreamingMock([
107
+ // First call: agent requests tool
108
+ {
109
+ textContent: "Let me check the weather",
110
+ toolCalls: [
111
+ {
112
+ id: "tool_1",
113
+ name: "get_weather",
114
+ input: { city: "Stockholm" },
115
+ },
116
+ ],
117
+ usage: { inputTokens: 10, outputTokens: 20 },
118
+ stopReason: "tool_use" as const,
119
+ },
120
+ // Second call: agent responds with result
121
+ {
122
+ textContent: "It's sunny and 22°C",
123
+ toolCalls: [],
124
+ usage: { inputTokens: 15, outputTokens: 10 },
125
+ stopReason: "end_turn" as const,
126
+ },
127
+ ]);
128
+
129
+ const agentProps: FlinkAgentProps<typeof mockCtx> = {
130
+ id: "weather_agent",
131
+ description: "Weather assistant",
132
+ instructions: "You help with weather",
133
+ tools: ["get_weather"],
134
+ };
135
+
136
+ const llmAdapters = new Map();
137
+ llmAdapters.set("default", weatherMockAdapter);
138
+ const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
139
+
140
+ const generator = runner.streamGenerator({ message: "What's the weather in Stockholm?" });
141
+
142
+ for await (const chunk of generator) {
143
+ if (chunk.type === "complete") {
144
+ expect(chunk.result.message).toBe("It's sunny and 22°C");
145
+ expect(chunk.result.toolCalls.length).toBe(1);
146
+ expect(chunk.result.toolCalls[0].name).toBe("get_weather");
147
+ expect(chunk.result.toolCalls[0].input).toEqual({ city: "Stockholm" });
148
+ expect(chunk.result.toolCalls[0].output).toEqual({
149
+ temperature: 22,
150
+ conditions: "sunny",
151
+ });
152
+ expect(chunk.result.stepsUsed).toBe(2);
153
+ }
154
+ }
155
+
156
+ // Verify tool was called
157
+ expect(toolFn).toHaveBeenCalledWith({
158
+ input: { city: "Stockholm" },
159
+ ctx: mockCtx,
160
+ user: undefined,
161
+ permissions: undefined,
162
+ conversationCtx: undefined,
163
+ });
164
+ });
165
+
166
+ it("should handle tool errors gracefully", async () => {
167
+ // Create mock tool that returns error
168
+ const toolProps: FlinkToolProps = {
169
+ id: "get_weather",
170
+ description: "Get weather",
171
+ inputSchema: z.object({ city: z.string() }),
172
+ };
173
+
174
+ const toolFn = jasmine.createSpy("toolFn").and.returnValue(
175
+ Promise.resolve({
176
+ success: false,
177
+ error: "API unavailable",
178
+ code: "SERVICE_ERROR",
179
+ })
180
+ );
181
+
182
+ const toolExecutor = new ToolExecutor(toolProps, toolFn as any, mockCtx);
183
+ const toolsMap = new Map([["get_weather", toolExecutor]]);
184
+
185
+ // Mock LLM adapter response with tool call
186
+ const errorMockAdapter = createStreamingMock([
187
+ // First call: agent requests tool
188
+ {
189
+ textContent: undefined,
190
+ toolCalls: [
191
+ {
192
+ id: "tool_1",
193
+ name: "get_weather",
194
+ input: { city: "Stockholm" },
195
+ },
196
+ ],
197
+ usage: { inputTokens: 10, outputTokens: 20 },
198
+ stopReason: "tool_use" as const,
199
+ },
200
+ // Second call: agent handles error
201
+ {
202
+ textContent: "Sorry, weather service is unavailable",
203
+ toolCalls: [],
204
+ usage: { inputTokens: 15, outputTokens: 10 },
205
+ stopReason: "end_turn" as const,
206
+ },
207
+ ]);
208
+
209
+ const agentProps: FlinkAgentProps<typeof mockCtx> = {
210
+ id: "weather_agent",
211
+ description: "Weather assistant",
212
+ instructions: "You help with weather",
213
+ tools: ["get_weather"],
214
+ };
215
+
216
+ const llmAdapters = new Map();
217
+ llmAdapters.set("default", errorMockAdapter);
218
+ const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
219
+
220
+ const generator = runner.streamGenerator({ message: "What's the weather?" });
221
+
222
+ for await (const chunk of generator) {
223
+ if (chunk.type === "complete") {
224
+ expect(chunk.result.toolCalls.length).toBe(1);
225
+ expect(chunk.result.toolCalls[0].error).toBe("API unavailable");
226
+ expect(chunk.result.toolCalls[0].output).toBeNull();
227
+ }
228
+ }
229
+ });
230
+
231
+ it("should handle missing tools", async () => {
232
+ const toolsMap = new Map(); // No tools registered
233
+
234
+ // Mock LLM adapter response requesting non-existent tool
235
+ const missingToolMockAdapter = createStreamingMock([
236
+ {
237
+ textContent: undefined,
238
+ toolCalls: [
239
+ {
240
+ id: "tool_1",
241
+ name: "missing_tool",
242
+ input: {},
243
+ },
244
+ ],
245
+ usage: { inputTokens: 10, outputTokens: 20 },
246
+ stopReason: "tool_use" as const,
247
+ },
248
+ {
249
+ textContent: "Tool not available",
250
+ toolCalls: [],
251
+ usage: { inputTokens: 15, outputTokens: 10 },
252
+ stopReason: "end_turn" as const,
253
+ },
254
+ ]);
255
+
256
+ const agentProps: FlinkAgentProps<typeof mockCtx> = {
257
+ id: "test_agent",
258
+ description: "Test agent",
259
+ instructions: "You are helpful",
260
+ tools: [],
261
+ };
262
+
263
+ const llmAdapters = new Map();
264
+ llmAdapters.set("default", missingToolMockAdapter);
265
+ const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
266
+
267
+ const generator = runner.streamGenerator({ message: "Test" });
268
+
269
+ for await (const chunk of generator) {
270
+ if (chunk.type === "complete") {
271
+ expect(chunk.result.toolCalls.length).toBe(1);
272
+ expect(chunk.result.toolCalls[0].error).toContain("not found");
273
+ }
274
+ }
275
+ });
276
+ });
277
+
278
+ describe("Step limits", () => {
279
+ it("should respect maxSteps limit", async () => {
280
+ // Mock infinite tool calling loop
281
+ const infiniteLoopMockAdapter = createStreamingMock([
282
+ {
283
+ textContent: undefined,
284
+ toolCalls: [
285
+ {
286
+ id: "tool_1",
287
+ name: "test_tool",
288
+ input: {},
289
+ },
290
+ ],
291
+ usage: { inputTokens: 10, outputTokens: 20 },
292
+ stopReason: "tool_use" as const,
293
+ },
294
+ ]);
295
+
296
+ const toolProps: FlinkToolProps = {
297
+ id: "test_tool",
298
+ description: "Test tool",
299
+ inputSchema: z.object({}),
300
+ };
301
+
302
+ const toolFn = async () => ({ success: true as const, data: {} });
303
+ const toolExecutor = new ToolExecutor(toolProps, toolFn, mockCtx);
304
+ const toolsMap = new Map([["test_tool", toolExecutor]]);
305
+
306
+ const agentProps: FlinkAgentProps<typeof mockCtx> = {
307
+ id: "test_agent",
308
+ description: "Test agent",
309
+ instructions: "You are helpful",
310
+ tools: ["test_tool"],
311
+ limits: { maxSteps: 3 },
312
+ };
313
+
314
+ const llmAdapters = new Map();
315
+ llmAdapters.set("default", infiniteLoopMockAdapter);
316
+ const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
317
+
318
+ const generator = runner.streamGenerator({ message: "Test" });
319
+
320
+ for await (const chunk of generator) {
321
+ if (chunk.type === "complete") {
322
+ expect(chunk.result.stepsUsed).toBe(3);
323
+ expect(chunk.result.stoppedEarly).toBe(true);
324
+ }
325
+ }
326
+ });
327
+
328
+ it("should allow runtime maxSteps override", async () => {
329
+ const runtimeOverrideMockAdapter = createStreamingMock([
330
+ {
331
+ textContent: undefined,
332
+ toolCalls: [
333
+ {
334
+ id: "tool_1",
335
+ name: "test_tool",
336
+ input: {},
337
+ },
338
+ ],
339
+ usage: { inputTokens: 10, outputTokens: 20 },
340
+ stopReason: "tool_use" as const,
341
+ },
342
+ ]);
343
+
344
+ const toolProps: FlinkToolProps = {
345
+ id: "test_tool",
346
+ description: "Test tool",
347
+ inputSchema: z.object({}),
348
+ };
349
+
350
+ const toolFn = async () => ({ success: true as const, data: {} });
351
+ const toolExecutor = new ToolExecutor(toolProps, toolFn, mockCtx);
352
+ const toolsMap = new Map([["test_tool", toolExecutor]]);
353
+
354
+ const agentProps: FlinkAgentProps<typeof mockCtx> = {
355
+ id: "test_agent",
356
+ description: "Test agent",
357
+ instructions: "You are helpful",
358
+ tools: ["test_tool"],
359
+ limits: { maxSteps: 10 },
360
+ };
361
+
362
+ const llmAdapters = new Map();
363
+ llmAdapters.set("default", runtimeOverrideMockAdapter);
364
+ const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
365
+
366
+ const generator = runner.streamGenerator({
367
+ message: "Test",
368
+ options: { maxSteps: 2 },
369
+ });
370
+
371
+ for await (const chunk of generator) {
372
+ if (chunk.type === "complete") {
373
+ expect(chunk.result.stepsUsed).toBe(2);
374
+ expect(chunk.result.stoppedEarly).toBe(true);
375
+ }
376
+ }
377
+ });
378
+ });
379
+
380
+ describe("Message format conversion", () => {
381
+ it("should convert string to message array", async () => {
382
+ const agentProps: FlinkAgentProps<typeof mockCtx> = {
383
+ id: "test_agent",
384
+ description: "Test agent",
385
+ instructions: "You are helpful",
386
+ tools: [],
387
+ };
388
+
389
+ const toolsMap = new Map();
390
+ const llmAdapters = new Map();
391
+ llmAdapters.set("default", mockLLMAdapter);
392
+ const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
393
+
394
+ const generator = runner.streamGenerator({ message: "Hello" });
395
+
396
+ for await (const chunk of generator) {
397
+ if (chunk.type === "complete") {
398
+ expect(chunk.result).toBeDefined();
399
+ }
400
+ }
401
+
402
+ // Verify LLM adapter was called with user message
403
+ expect(mockLLMAdapter.stream).toHaveBeenCalled();
404
+ const callArgs = (mockLLMAdapter.stream as jasmine.Spy).calls.first().args[0];
405
+ expect(callArgs.messages.length).toBeGreaterThanOrEqual(1);
406
+ expect(callArgs.messages[0]).toEqual({ role: "user", content: "Hello" });
407
+ });
408
+
409
+ it("should convert Message[] to Anthropic format", async () => {
410
+ const agentProps: FlinkAgentProps<typeof mockCtx> = {
411
+ id: "test_agent",
412
+ description: "Test agent",
413
+ instructions: "You are helpful",
414
+ tools: [],
415
+ };
416
+
417
+ const toolsMap = new Map();
418
+ const llmAdapters = new Map();
419
+ llmAdapters.set("default", mockLLMAdapter);
420
+ const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
421
+
422
+ const generator = runner.streamGenerator({
423
+ message: [
424
+ { role: "user", content: "Hello" },
425
+ { role: "user", content: "How are you?" },
426
+ ],
427
+ });
428
+
429
+ for await (const chunk of generator) {
430
+ if (chunk.type === "complete") {
431
+ expect(chunk.result).toBeDefined();
432
+ }
433
+ }
434
+
435
+ expect(mockLLMAdapter.stream).toHaveBeenCalled();
436
+ });
437
+ });
438
+
439
+ describe("Model configuration", () => {
440
+ it("should pass configuration to LLM adapter", async () => {
441
+ const agentProps: FlinkAgentProps<typeof mockCtx> = {
442
+ id: "test_agent",
443
+ description: "Test agent",
444
+ instructions: "You are helpful",
445
+ tools: [],
446
+ };
447
+
448
+ const toolsMap = new Map();
449
+ const llmAdapters = new Map();
450
+ llmAdapters.set("default", mockLLMAdapter);
451
+ const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
452
+
453
+ const generator = runner.streamGenerator({ message: "Hello" });
454
+
455
+ for await (const chunk of generator) {
456
+ if (chunk.type === "complete") {
457
+ break;
458
+ }
459
+ }
460
+
461
+ // Verify LLM adapter was called via stream
462
+ expect(mockLLMAdapter.stream).toHaveBeenCalled();
463
+ const callArgs = (mockLLMAdapter.stream as jasmine.Spy).calls.mostRecent().args[0];
464
+ expect(callArgs.instructions).toBe("You are helpful");
465
+ });
466
+
467
+ it("should pass custom temperature and maxTokens", async () => {
468
+ const agentProps: FlinkAgentProps<typeof mockCtx> = {
469
+ id: "test_agent",
470
+ description: "Test agent",
471
+ instructions: "You are helpful",
472
+ tools: [],
473
+ model: {
474
+ temperature: 0.3,
475
+ maxTokens: 2000,
476
+ },
477
+ };
478
+
479
+ const toolsMap = new Map();
480
+ const llmAdapters = new Map();
481
+ llmAdapters.set("default", mockLLMAdapter);
482
+ const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
483
+
484
+ const generator = runner.streamGenerator({ message: "Hello" });
485
+
486
+ for await (const chunk of generator) {
487
+ if (chunk.type === "complete") {
488
+ break;
489
+ }
490
+ }
491
+
492
+ const callArgs = (mockLLMAdapter.stream as jasmine.Spy).calls.mostRecent().args[0];
493
+ expect(callArgs.temperature).toBe(0.3);
494
+ expect(callArgs.maxTokens).toBe(2000);
495
+ });
496
+ });
497
+
498
+ describe("Error handling", () => {
499
+ it("should throw error when LLM adapter not configured", () => {
500
+ const agentProps: FlinkAgentProps<typeof mockCtx> = {
501
+ id: "test_agent",
502
+ description: "Test agent",
503
+ instructions: "You are helpful",
504
+ tools: [],
505
+ };
506
+
507
+ const toolsMap = new Map();
508
+ const llmAdapters = new Map(); // Empty map
509
+
510
+ expect(() => {
511
+ new AgentRunner(agentProps, toolsMap, llmAdapters);
512
+ }).toThrowError(/not configured/);
513
+ });
514
+
515
+ it("should throw error for unregistered adapter", () => {
516
+ const agentProps: FlinkAgentProps<typeof mockCtx> = {
517
+ id: "test_agent",
518
+ description: "Test agent",
519
+ instructions: "You are helpful",
520
+ tools: [],
521
+ model: {
522
+ adapterId: "openai",
523
+ },
524
+ };
525
+
526
+ const toolsMap = new Map();
527
+ const llmAdapters = new Map();
528
+ llmAdapters.set("default", mockLLMAdapter);
529
+
530
+ expect(() => {
531
+ new AgentRunner(agentProps, toolsMap, llmAdapters);
532
+ }).toThrowError(/LLM adapter "openai" not configured/);
533
+ });
534
+ });
535
+
536
+ describe("Message Conversion with Conversation History", () => {
537
+ it("should preserve assistant messages without tool calls", async () => {
538
+ const agentProps: FlinkAgentProps<typeof mockCtx> = {
539
+ id: "test_agent",
540
+ description: "Test agent",
541
+ instructions: "You are helpful",
542
+ tools: [],
543
+ };
544
+
545
+ const toolsMap = new Map();
546
+ const llmAdapters = new Map();
547
+ llmAdapters.set("default", mockLLMAdapter);
548
+ const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
549
+
550
+ const history = [
551
+ { role: "user" as const, content: "Who are you?" },
552
+ { role: "assistant" as const, content: "I'm a weather assistant." },
553
+ ];
554
+
555
+ const generator = runner.streamGenerator({
556
+ message: "What's the weather?",
557
+ history,
558
+ });
559
+
560
+ for await (const chunk of generator) {
561
+ if (chunk.type === "complete") {
562
+ break;
563
+ }
564
+ }
565
+
566
+ // Verify LLM adapter was called with history preserved
567
+ expect(mockLLMAdapter.stream).toHaveBeenCalled();
568
+ const callArgs = (mockLLMAdapter.stream as jasmine.Spy).calls.first().args[0];
569
+ const messages = callArgs.messages;
570
+
571
+ // Note: messages array gets mutated after LLM call (assistant message added)
572
+ // So we check that the first 3 messages are correct (history + new user message)
573
+ expect(messages.length).toBeGreaterThanOrEqual(3);
574
+ expect(messages[0]).toEqual({ role: "user", content: "Who are you?" });
575
+ expect(messages[1]).toEqual({ role: "assistant", content: "I'm a weather assistant." });
576
+ expect(messages[2]).toEqual({ role: "user", content: "What's the weather?" });
577
+
578
+ // Verify assistant message from history was preserved (not filtered out)
579
+ expect(messages[1].role).toBe("assistant");
580
+ expect(messages[1].content).toBe("I'm a weather assistant.");
581
+ });
582
+
583
+ it("should preserve assistant messages with tool calls", async () => {
584
+ const agentProps: FlinkAgentProps<typeof mockCtx> = {
585
+ id: "test_agent",
586
+ description: "Test agent",
587
+ instructions: "You are helpful",
588
+ tools: [],
589
+ };
590
+
591
+ const toolsMap = new Map();
592
+ const llmAdapters = new Map();
593
+ llmAdapters.set("default", mockLLMAdapter);
594
+ const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
595
+
596
+ const history = [
597
+ { role: "user" as const, content: "What's the weather in Stockholm?" },
598
+ {
599
+ role: "assistant" as const,
600
+ content: "",
601
+ toolCalls: [{ id: "1", name: "get-weather", input: { city: "Stockholm" } }],
602
+ },
603
+ { role: "tool" as const, toolCallId: "1", toolName: "get-weather", result: "22°C, Sunny" },
604
+ ];
605
+
606
+ const generator = runner.streamGenerator({
607
+ message: "And tomorrow?",
608
+ history,
609
+ });
610
+
611
+ for await (const chunk of generator) {
612
+ if (chunk.type === "complete") {
613
+ break;
614
+ }
615
+ }
616
+
617
+ expect(mockLLMAdapter.stream).toHaveBeenCalled();
618
+ const callArgs = (mockLLMAdapter.stream as jasmine.Spy).calls.first().args[0];
619
+ const messages = callArgs.messages;
620
+
621
+ // Note: messages array gets mutated after LLM call
622
+ // Check first 4 messages: history (3) + new user message (1)
623
+ expect(messages.length).toBeGreaterThanOrEqual(4);
624
+ expect(messages[0].role).toBe("user");
625
+ expect(messages[1].role).toBe("assistant");
626
+ expect(messages[2].role).toBe("user"); // Tool result
627
+ expect(messages[3].role).toBe("user"); // New message
628
+
629
+ // Assistant message should have tool_use content blocks (preserved from history)
630
+ expect(Array.isArray(messages[1].content)).toBe(true);
631
+ const contentBlocks = messages[1].content as any[];
632
+ expect(contentBlocks.length).toBe(1);
633
+ expect(contentBlocks[0].type).toBe("tool_use");
634
+ expect(contentBlocks[0].name).toBe("get-weather");
635
+ expect(contentBlocks[0].input).toEqual({ city: "Stockholm" });
636
+ });
637
+
638
+ it("should maintain alternating user/assistant pattern", async () => {
639
+ const agentProps: FlinkAgentProps<typeof mockCtx> = {
640
+ id: "test_agent",
641
+ description: "Test agent",
642
+ instructions: "You are helpful",
643
+ tools: [],
644
+ };
645
+
646
+ const toolsMap = new Map();
647
+ const llmAdapters = new Map();
648
+ llmAdapters.set("default", mockLLMAdapter);
649
+ const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
650
+
651
+ const history = [
652
+ { role: "user" as const, content: "Hello" },
653
+ { role: "assistant" as const, content: "Hi there!" },
654
+ { role: "user" as const, content: "How are you?" },
655
+ { role: "assistant" as const, content: "I'm doing well!" },
656
+ ];
657
+
658
+ const generator = runner.streamGenerator({
659
+ message: "What's your name?",
660
+ history,
661
+ });
662
+
663
+ for await (const chunk of generator) {
664
+ if (chunk.type === "complete") {
665
+ break;
666
+ }
667
+ }
668
+
669
+ expect(mockLLMAdapter.stream).toHaveBeenCalled();
670
+ const callArgs = (mockLLMAdapter.stream as jasmine.Spy).calls.first().args[0];
671
+ const messages = callArgs.messages;
672
+
673
+ // Note: messages array gets mutated after LLM call
674
+ // Check first 5 messages: history (4) + new user message (1)
675
+ expect(messages.length).toBeGreaterThanOrEqual(5);
676
+
677
+ // Verify no consecutive user or assistant messages in the first 5 (input messages)
678
+ for (let i = 1; i < Math.min(5, messages.length); i++) {
679
+ expect(messages[i].role).not.toBe(messages[i - 1].role);
680
+ }
681
+ });
682
+
683
+ it("should handle assistant messages with both text and tool calls", async () => {
684
+ const agentProps: FlinkAgentProps<typeof mockCtx> = {
685
+ id: "test_agent",
686
+ description: "Test agent",
687
+ instructions: "You are helpful",
688
+ tools: [],
689
+ };
690
+
691
+ const toolsMap = new Map();
692
+ const llmAdapters = new Map();
693
+ llmAdapters.set("default", mockLLMAdapter);
694
+ const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
695
+
696
+ const history = [
697
+ { role: "user" as const, content: "Check the weather" },
698
+ {
699
+ role: "assistant" as const,
700
+ content: "Let me check that for you",
701
+ toolCalls: [{ id: "1", name: "get-weather", input: { city: "Stockholm" } }],
702
+ },
703
+ ];
704
+
705
+ const generator = runner.streamGenerator({ message: history });
706
+
707
+ for await (const chunk of generator) {
708
+ if (chunk.type === "complete") {
709
+ break;
710
+ }
711
+ }
712
+
713
+ expect(mockLLMAdapter.stream).toHaveBeenCalled();
714
+ const callArgs = (mockLLMAdapter.stream as jasmine.Spy).calls.first().args[0];
715
+ const messages = callArgs.messages;
716
+
717
+ // Assistant message should have both text and tool_use blocks
718
+ expect(Array.isArray(messages[1].content)).toBe(true);
719
+ const contentBlocks = messages[1].content as any[];
720
+ expect(contentBlocks.length).toBe(2);
721
+ expect(contentBlocks[0].type).toBe("text");
722
+ expect(contentBlocks[0].text).toBe("Let me check that for you");
723
+ expect(contentBlocks[1].type).toBe("tool_use");
724
+ expect(contentBlocks[1].name).toBe("get-weather");
725
+ });
726
+
727
+ it("should convert tool messages to user messages with results", async () => {
728
+ const agentProps: FlinkAgentProps<typeof mockCtx> = {
729
+ id: "test_agent",
730
+ description: "Test agent",
731
+ instructions: "You are helpful",
732
+ tools: [],
733
+ };
734
+
735
+ const toolsMap = new Map();
736
+ const llmAdapters = new Map();
737
+ llmAdapters.set("default", mockLLMAdapter);
738
+ const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
739
+
740
+ const history = [
741
+ { role: "user" as const, content: "Check weather" },
742
+ {
743
+ role: "assistant" as const,
744
+ content: "",
745
+ toolCalls: [{ id: "1", name: "get-weather", input: { city: "Stockholm" } }],
746
+ },
747
+ { role: "tool" as const, toolCallId: "1", toolName: "get-weather", result: "22°C" },
748
+ ];
749
+
750
+ const generator = runner.streamGenerator({ message: history });
751
+
752
+ for await (const chunk of generator) {
753
+ if (chunk.type === "complete") {
754
+ break;
755
+ }
756
+ }
757
+
758
+ expect(mockLLMAdapter.stream).toHaveBeenCalled();
759
+ const callArgs = (mockLLMAdapter.stream as jasmine.Spy).calls.first().args[0];
760
+ const messages = callArgs.messages;
761
+
762
+ // Tool message should be converted to user message with tool_result content block
763
+ expect(messages[2].role).toBe("user");
764
+ expect(Array.isArray(messages[2].content)).toBe(true);
765
+ const contentBlocks = messages[2].content as any[];
766
+ expect(contentBlocks.length).toBe(1);
767
+ expect(contentBlocks[0].type).toBe("tool_result");
768
+ expect(contentBlocks[0].tool_use_id).toBe("1");
769
+ expect(contentBlocks[0].content).toBe("22°C");
770
+ });
771
+
772
+ it("should handle empty content with tool calls", async () => {
773
+ const agentProps: FlinkAgentProps<typeof mockCtx> = {
774
+ id: "test_agent",
775
+ description: "Test agent",
776
+ instructions: "You are helpful",
777
+ tools: [],
778
+ };
779
+
780
+ const toolsMap = new Map();
781
+ const llmAdapters = new Map();
782
+ llmAdapters.set("default", mockLLMAdapter);
783
+ const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
784
+
785
+ const history = [
786
+ { role: "user" as const, content: "Check weather" },
787
+ {
788
+ role: "assistant" as const,
789
+ content: "", // Empty content
790
+ toolCalls: [{ id: "1", name: "get-weather", input: {} }],
791
+ },
792
+ ];
793
+
794
+ const generator = runner.streamGenerator({ message: history });
795
+
796
+ for await (const chunk of generator) {
797
+ if (chunk.type === "complete") {
798
+ break;
799
+ }
800
+ }
801
+
802
+ expect(mockLLMAdapter.stream).toHaveBeenCalled();
803
+ const callArgs = (mockLLMAdapter.stream as jasmine.Spy).calls.first().args[0];
804
+ const messages = callArgs.messages;
805
+
806
+ // Assistant message should only have tool_use block (no text block for empty content)
807
+ expect(Array.isArray(messages[1].content)).toBe(true);
808
+ const contentBlocks = messages[1].content as any[];
809
+ expect(contentBlocks.length).toBe(1);
810
+ expect(contentBlocks[0].type).toBe("tool_use");
811
+ });
812
+ });
813
+
814
+ describe("Dynamic Instructions", () => {
815
+ it("should use static string instructions (backwards compatibility)", async () => {
816
+ const agentProps: FlinkAgentProps<typeof mockCtx> = {
817
+ id: "test_agent",
818
+ description: "Test agent",
819
+ instructions: "Static instructions",
820
+ tools: [],
821
+ };
822
+
823
+ const toolsMap = new Map();
824
+ const llmAdapters = new Map();
825
+ llmAdapters.set("default", mockLLMAdapter);
826
+ const runner = new AgentRunner(agentProps, toolsMap, llmAdapters, "test_agent", mockCtx);
827
+
828
+ const generator = runner.streamGenerator({ message: "Hello" });
829
+
830
+ for await (const chunk of generator) {
831
+ if (chunk.type === "complete") {
832
+ break;
833
+ }
834
+ }
835
+
836
+ // Verify LLM was called with static instructions
837
+ expect(mockLLMAdapter.stream).toHaveBeenCalled();
838
+ const callArgs = (mockLLMAdapter.stream as jasmine.Spy).calls.first().args[0];
839
+ expect(callArgs.instructions).toBe("Static instructions");
840
+ });
841
+
842
+ it("should resolve synchronous callback instructions", async () => {
843
+ const instructionsCallback = jasmine.createSpy("instructionsCallback").and.returnValue("Dynamic: test-user");
844
+
845
+ const agentProps: FlinkAgentProps<typeof mockCtx> = {
846
+ id: "test_agent",
847
+ description: "Test agent",
848
+ instructions: instructionsCallback,
849
+ tools: [],
850
+ };
851
+
852
+ const toolsMap = new Map();
853
+ const llmAdapters = new Map();
854
+ llmAdapters.set("default", mockLLMAdapter);
855
+ const runner = new AgentRunner(agentProps, toolsMap, llmAdapters, "test_agent", mockCtx);
856
+
857
+ const generator = runner.streamGenerator({
858
+ message: "Hello",
859
+ user: { id: "123", name: "test-user" },
860
+ });
861
+
862
+ for await (const chunk of generator) {
863
+ if (chunk.type === "complete") {
864
+ break;
865
+ }
866
+ }
867
+
868
+ // Verify callback was called with correct context
869
+ expect(instructionsCallback).toHaveBeenCalledTimes(1);
870
+ const callArgs = instructionsCallback.calls.first().args;
871
+ expect(callArgs[0]).toBe(mockCtx); // ctx
872
+ expect(callArgs[1].user).toEqual({ id: "123", name: "test-user" }); // agentContext
873
+
874
+ // Verify LLM was called with resolved instructions
875
+ expect(mockLLMAdapter.stream).toHaveBeenCalled();
876
+ const llmCallArgs = (mockLLMAdapter.stream as jasmine.Spy).calls.first().args[0];
877
+ expect(llmCallArgs.instructions).toBe("Dynamic: test-user");
878
+ });
879
+
880
+ it("should resolve async callback instructions", async () => {
881
+ const mockUser = { id: "123", name: "test-user", tier: "premium" };
882
+ const mockRepoCtx = {
883
+ repos: {
884
+ userRepo: {
885
+ getById: jasmine.createSpy("getById").and.returnValue(Promise.resolve(mockUser)),
886
+ },
887
+ },
888
+ plugins: {},
889
+ };
890
+
891
+ const instructionsCallback = async (ctx: any, agentContext: any) => {
892
+ const profile = await ctx.repos.userRepo.getById(agentContext.user.id);
893
+ return `You are a support agent. Customer: ${profile.name}, Tier: ${profile.tier}`;
894
+ };
895
+
896
+ const agentProps: FlinkAgentProps<typeof mockCtx> = {
897
+ id: "test_agent",
898
+ description: "Test agent",
899
+ instructions: instructionsCallback,
900
+ tools: [],
901
+ };
902
+
903
+ const toolsMap = new Map();
904
+ const llmAdapters = new Map();
905
+ llmAdapters.set("default", mockLLMAdapter);
906
+ const runner = new AgentRunner(agentProps, toolsMap, llmAdapters, "test_agent", mockRepoCtx);
907
+
908
+ const generator = runner.streamGenerator({
909
+ message: "Hello",
910
+ user: { id: "123" },
911
+ });
912
+
913
+ for await (const chunk of generator) {
914
+ if (chunk.type === "complete") {
915
+ break;
916
+ }
917
+ }
918
+
919
+ // Verify repo was called
920
+ expect(mockRepoCtx.repos.userRepo.getById).toHaveBeenCalledWith("123");
921
+
922
+ // Verify LLM was called with resolved instructions
923
+ expect(mockLLMAdapter.stream).toHaveBeenCalled();
924
+ const llmCallArgs = (mockLLMAdapter.stream as jasmine.Spy).calls.first().args[0];
925
+ expect(llmCallArgs.instructions).toBe("You are a support agent. Customer: test-user, Tier: premium");
926
+ });
927
+
928
+ it("should pass correct context to callback", async () => {
929
+ const instructionsCallback = jasmine.createSpy("instructionsCallback").and.returnValue("Instructions");
930
+
931
+ const agentProps: FlinkAgentProps<typeof mockCtx> = {
932
+ id: "test_agent",
933
+ description: "Test agent",
934
+ instructions: instructionsCallback,
935
+ tools: [],
936
+ };
937
+
938
+ const toolsMap = new Map();
939
+ const llmAdapters = new Map();
940
+ llmAdapters.set("default", mockLLMAdapter);
941
+ const runner = new AgentRunner(agentProps, toolsMap, llmAdapters, "test_agent", mockCtx);
942
+
943
+ const generator = runner.streamGenerator({
944
+ message: "Hello",
945
+ user: { id: "123" },
946
+ conversationId: "conv-456",
947
+ metadata: { source: "handler" },
948
+ });
949
+
950
+ for await (const chunk of generator) {
951
+ if (chunk.type === "complete") {
952
+ break;
953
+ }
954
+ }
955
+
956
+ // Verify callback received correct context
957
+ expect(instructionsCallback).toHaveBeenCalledTimes(1);
958
+ const callArgs = instructionsCallback.calls.first().args;
959
+
960
+ // Check ctx parameter
961
+ expect(callArgs[0]).toBe(mockCtx);
962
+
963
+ // Check execContext parameter
964
+ const execContext = callArgs[1];
965
+ expect(execContext.agentId).toBe("test_agent");
966
+ expect(execContext.conversationId).toBe("conv-456");
967
+ expect(execContext.user).toEqual({ id: "123" });
968
+ expect(execContext.metadata).toEqual({ source: "handler" });
969
+ });
970
+
971
+ it("should handle callback errors gracefully", async () => {
972
+ const instructionsCallback = () => {
973
+ throw new Error("Database connection failed");
974
+ };
975
+
976
+ const agentProps: FlinkAgentProps<typeof mockCtx> = {
977
+ id: "test_agent",
978
+ description: "Test agent",
979
+ instructions: instructionsCallback,
980
+ tools: [],
981
+ };
982
+
983
+ const toolsMap = new Map();
984
+ const llmAdapters = new Map();
985
+ llmAdapters.set("default", mockLLMAdapter);
986
+ const runner = new AgentRunner(agentProps, toolsMap, llmAdapters, "test_agent", mockCtx);
987
+
988
+ const generator = runner.streamGenerator({ message: "Hello" });
989
+
990
+ try {
991
+ for await (const chunk of generator) {
992
+ // Should not reach here
993
+ }
994
+ fail("Expected error to be thrown");
995
+ } catch (err: any) {
996
+ expect(err.message).toContain("Failed to resolve instructions for agent test_agent");
997
+ expect(err.message).toContain("Database connection failed");
998
+ }
999
+ });
1000
+
1001
+ it("should resolve instructions once per execution", async () => {
1002
+ let callCount = 0;
1003
+ const instructionsCallback = () => {
1004
+ callCount++;
1005
+ return "Instructions";
1006
+ };
1007
+
1008
+ // Mock adapter that calls LLM 3 times (3 steps with tool calls)
1009
+ const multiStepMockAdapter = createStreamingMock([
1010
+ {
1011
+ textContent: undefined,
1012
+ toolCalls: [{ id: "tool_1", name: "test_tool", input: {} }],
1013
+ usage: { inputTokens: 10, outputTokens: 20 },
1014
+ stopReason: "tool_use" as const,
1015
+ },
1016
+ {
1017
+ textContent: undefined,
1018
+ toolCalls: [{ id: "tool_2", name: "test_tool", input: {} }],
1019
+ usage: { inputTokens: 10, outputTokens: 20 },
1020
+ stopReason: "tool_use" as const,
1021
+ },
1022
+ {
1023
+ textContent: "Final response",
1024
+ toolCalls: [],
1025
+ usage: { inputTokens: 10, outputTokens: 20 },
1026
+ stopReason: "end_turn" as const,
1027
+ },
1028
+ ]);
1029
+
1030
+ const toolProps: FlinkToolProps = {
1031
+ id: "test_tool",
1032
+ description: "Test tool",
1033
+ inputSchema: z.object({}),
1034
+ };
1035
+ const toolFn = async () => ({ success: true as const, data: {} });
1036
+ const toolExecutor = new ToolExecutor(toolProps, toolFn, mockCtx);
1037
+ const toolsMap = new Map([["test_tool", toolExecutor]]);
1038
+
1039
+ const agentProps: FlinkAgentProps<typeof mockCtx> = {
1040
+ id: "test_agent",
1041
+ description: "Test agent",
1042
+ instructions: instructionsCallback,
1043
+ tools: ["test_tool"],
1044
+ };
1045
+
1046
+ const llmAdapters = new Map();
1047
+ llmAdapters.set("default", multiStepMockAdapter);
1048
+ const runner = new AgentRunner(agentProps, toolsMap, llmAdapters, "test_agent", mockCtx);
1049
+
1050
+ const generator = runner.streamGenerator({ message: "Hello" });
1051
+
1052
+ for await (const chunk of generator) {
1053
+ if (chunk.type === "complete") {
1054
+ expect(chunk.result.stepsUsed).toBe(3);
1055
+ }
1056
+ }
1057
+
1058
+ // Callback should be called ONCE, not 3 times
1059
+ expect(callCount).toBe(1);
1060
+ });
1061
+ });
1062
+ });