@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,520 @@
1
+ import {
2
+ ConversationAgent,
3
+ ConversationData,
4
+ StorageMessage,
5
+ FlinkContext,
6
+ } from "../../src";
7
+
8
+ // Mock agent for testing
9
+ class MockConversationAgent extends ConversationAgent<FlinkContext> {
10
+ id = "mock-agent";
11
+ description = "Mock agent for testing";
12
+ instructions() { return "You are a test agent"; }
13
+ tools = [];
14
+
15
+ // Spy methods
16
+ loadConversationSpy = jasmine.createSpy("loadConversation");
17
+ saveConversationSpy = jasmine.createSpy("saveConversation");
18
+
19
+ protected async loadConversation(
20
+ conversationId: string
21
+ ): Promise<ConversationData | null> {
22
+ return this.loadConversationSpy(conversationId);
23
+ }
24
+
25
+ protected async saveConversation(
26
+ conversationId: string,
27
+ data: ConversationData
28
+ ): Promise<void> {
29
+ return this.saveConversationSpy(conversationId, data);
30
+ }
31
+
32
+ // Expose protected methods for testing
33
+ public testBeforeRun(input: any, context: any) {
34
+ return (this as any).beforeRun(input, context);
35
+ }
36
+
37
+ public testAfterRun(result: any, context: any) {
38
+ return (this as any).afterRun(result, context);
39
+ }
40
+
41
+ public testConvertToMessages(llmMessages: any[]): StorageMessage[] {
42
+ return (this as any).convertToMessages(llmMessages);
43
+ }
44
+ }
45
+
46
+ describe("ConversationAgent", () => {
47
+ let agent: MockConversationAgent;
48
+ let mockContext: FlinkContext;
49
+
50
+ beforeEach(() => {
51
+ mockContext = {} as FlinkContext;
52
+ agent = new MockConversationAgent();
53
+ (agent as any).ctx = mockContext;
54
+ });
55
+
56
+ describe("beforeRun", () => {
57
+ it("should load conversation when conversationId is provided", async () => {
58
+ const conversationId = "conv-123";
59
+ const conversationData: ConversationData = {
60
+ messages: [
61
+ { role: "user", content: "Hello" },
62
+ { role: "assistant", content: "Hi there" },
63
+ ],
64
+ providerMetadata: {
65
+ openai: { responseId: "resp-123", instructionsHash: "hash-456" },
66
+ },
67
+ };
68
+
69
+ agent.loadConversationSpy.and.returnValue(
70
+ Promise.resolve(conversationData)
71
+ );
72
+
73
+ const input: any = { conversationId };
74
+ const context: any = {};
75
+
76
+ await agent.testBeforeRun(input, context);
77
+
78
+ expect(agent.loadConversationSpy).toHaveBeenCalledWith(
79
+ conversationId
80
+ );
81
+ expect(input.history).toBeDefined();
82
+ expect(input.history.length).toBe(2);
83
+ expect(input.providerMetadata).toEqual(
84
+ conversationData.providerMetadata
85
+ );
86
+ });
87
+
88
+ it("should not populate history if conversation not found", async () => {
89
+ const conversationId = "conv-nonexistent";
90
+
91
+ agent.loadConversationSpy.and.returnValue(Promise.resolve(null));
92
+
93
+ const input: any = { conversationId };
94
+ const context: any = {};
95
+
96
+ await agent.testBeforeRun(input, context);
97
+
98
+ expect(agent.loadConversationSpy).toHaveBeenCalledWith(
99
+ conversationId
100
+ );
101
+ expect(input.history).toBeUndefined();
102
+ expect(input.providerMetadata).toBeUndefined();
103
+ });
104
+
105
+ it("should not call loadConversation if conversationId not provided", async () => {
106
+ const input: any = {};
107
+ const context: any = {};
108
+
109
+ await agent.testBeforeRun(input, context);
110
+
111
+ expect(agent.loadConversationSpy).not.toHaveBeenCalled();
112
+ expect(input.history).toBeUndefined();
113
+ });
114
+
115
+ it("should propagate load errors", async () => {
116
+ const conversationId = "conv-error";
117
+ const error = new Error("Database connection failed");
118
+
119
+ agent.loadConversationSpy.and.returnValue(Promise.reject(error));
120
+
121
+ const input: any = { conversationId };
122
+ const context: any = {};
123
+
124
+ await expectAsync(
125
+ agent.testBeforeRun(input, context)
126
+ ).toBeRejectedWith(error);
127
+ });
128
+ });
129
+
130
+ describe("provider metadata", () => {
131
+ it("should always preserve provider metadata", async () => {
132
+ const conversationData: ConversationData = {
133
+ messages: [{ role: "user", content: "Hello" }],
134
+ providerMetadata: { openai: { responseId: "resp-123" } },
135
+ };
136
+
137
+ agent.loadConversationSpy.and.returnValue(
138
+ Promise.resolve(conversationData)
139
+ );
140
+
141
+ const input: any = { conversationId: "conv-123" };
142
+ await agent.testBeforeRun(input, {});
143
+
144
+ // Metadata is always preserved (no strategy filtering)
145
+ expect(input.providerMetadata).toEqual({
146
+ openai: { responseId: "resp-123" },
147
+ });
148
+ });
149
+ });
150
+
151
+ describe("afterRun", () => {
152
+ it("should save conversation after successful execution", async () => {
153
+ const conversationId = "conv-123";
154
+ const llmMessages = [
155
+ {
156
+ role: "user",
157
+ content: [{ type: "text", text: "What cars do you have?" }],
158
+ },
159
+ {
160
+ role: "assistant",
161
+ content: [{ type: "text", text: "We have sedans and SUVs" }],
162
+ },
163
+ ];
164
+ const providerMetadata = {
165
+ openai: { responseId: "resp-456", instructionsHash: "hash-789" },
166
+ };
167
+
168
+ agent.saveConversationSpy.and.returnValue(Promise.resolve());
169
+
170
+ const result: any = {
171
+ message: "Response",
172
+ toolCalls: [],
173
+ stepsUsed: 1,
174
+ stoppedEarly: false,
175
+ providerMetadata,
176
+ };
177
+
178
+ const context: any = {
179
+ messages: llmMessages,
180
+ conversationId,
181
+ };
182
+
183
+ await agent.testAfterRun(result, context);
184
+
185
+ expect(agent.saveConversationSpy).toHaveBeenCalled();
186
+
187
+ const [savedConvId, savedData] =
188
+ agent.saveConversationSpy.calls.argsFor(0);
189
+ expect(savedConvId).toBe(conversationId);
190
+ expect(savedData.messages).toEqual([
191
+ { role: "user", content: "What cars do you have?" },
192
+ { role: "assistant", content: "We have sedans and SUVs" },
193
+ ]);
194
+ expect(savedData.providerMetadata).toEqual(providerMetadata);
195
+ });
196
+
197
+ it("should skip save when conversationId not provided", async () => {
198
+ const result: any = {
199
+ message: "Response",
200
+ toolCalls: [],
201
+ stepsUsed: 1,
202
+ stoppedEarly: false,
203
+ };
204
+
205
+ const context: any = {
206
+ messages: [],
207
+ input: {},
208
+ };
209
+
210
+ await agent.testAfterRun(result, context);
211
+
212
+ expect(agent.saveConversationSpy).not.toHaveBeenCalled();
213
+ });
214
+
215
+ it("should not throw on save errors (log only)", async () => {
216
+ const conversationId = "conv-123";
217
+ const error = new Error("Database write failed");
218
+
219
+ agent.saveConversationSpy.and.returnValue(Promise.reject(error));
220
+
221
+ const result: any = {
222
+ message: "Response",
223
+ toolCalls: [],
224
+ stepsUsed: 1,
225
+ stoppedEarly: false,
226
+ };
227
+
228
+ const context: any = {
229
+ messages: [
230
+ {
231
+ role: "user",
232
+ content: [{ type: "text", text: "Hello" }],
233
+ },
234
+ ],
235
+ conversationId,
236
+ };
237
+
238
+ // Should not throw - just log error
239
+ await expectAsync(agent.testAfterRun(result, context)).toBeResolved();
240
+
241
+ expect(agent.saveConversationSpy).toHaveBeenCalled();
242
+ });
243
+ });
244
+
245
+ describe("convertToAgentMessages (loading from storage)", () => {
246
+ it("should convert user messages with toolResults to tool Messages", async () => {
247
+ const conversationData: ConversationData = {
248
+ messages: [
249
+ { role: "user", content: "What is the weather?" },
250
+ { role: "assistant", content: "Let me check" },
251
+ {
252
+ role: "user",
253
+ toolResults: [
254
+ { id: "call_1", result: { temp: "22C", condition: "sunny" } },
255
+ ],
256
+ },
257
+ ],
258
+ };
259
+
260
+ agent.loadConversationSpy.and.returnValue(Promise.resolve(conversationData));
261
+
262
+ const input: any = { conversationId: "conv-123" };
263
+ await agent.testBeforeRun(input, {});
264
+
265
+ expect(input.history).toBeDefined();
266
+ expect(input.history.length).toBe(3);
267
+
268
+ // First message: user
269
+ expect(input.history[0]).toEqual({
270
+ role: "user",
271
+ content: "What is the weather?",
272
+ });
273
+
274
+ // Second message: assistant
275
+ expect(input.history[1]).toEqual({
276
+ role: "assistant",
277
+ content: "Let me check",
278
+ toolCalls: undefined,
279
+ });
280
+
281
+ // Third message: tool result (converted from user with toolResults)
282
+ expect(input.history[2]).toEqual({
283
+ role: "tool",
284
+ toolCallId: "call_1",
285
+ toolName: "",
286
+ result: JSON.stringify({ temp: "22C", condition: "sunny" }),
287
+ });
288
+ });
289
+
290
+ it("should convert multiple tool results to multiple tool Messages", async () => {
291
+ const conversationData: ConversationData = {
292
+ messages: [
293
+ {
294
+ role: "user",
295
+ toolResults: [
296
+ { id: "call_1", result: { location: "Stockholm" } },
297
+ { id: "call_2", result: { temp: "22C" } },
298
+ ],
299
+ },
300
+ ],
301
+ };
302
+
303
+ agent.loadConversationSpy.and.returnValue(Promise.resolve(conversationData));
304
+
305
+ const input: any = { conversationId: "conv-123" };
306
+ await agent.testBeforeRun(input, {});
307
+
308
+ expect(input.history).toBeDefined();
309
+ expect(input.history.length).toBe(2);
310
+
311
+ // Each tool result becomes a separate Message
312
+ expect(input.history[0]).toEqual({
313
+ role: "tool",
314
+ toolCallId: "call_1",
315
+ toolName: "",
316
+ result: JSON.stringify({ location: "Stockholm" }),
317
+ });
318
+
319
+ expect(input.history[1]).toEqual({
320
+ role: "tool",
321
+ toolCallId: "call_2",
322
+ toolName: "",
323
+ result: JSON.stringify({ temp: "22C" }),
324
+ });
325
+ });
326
+
327
+ it("should not double-encode string tool results on reload", async () => {
328
+ // Real production flow: ToolExecutor.formatResultForAI returns a JSON string,
329
+ // which is what AgentRunner stores on tool_result content blocks. On the
330
+ // round-trip back from storage, that string must NOT be JSON.stringified
331
+ // again — otherwise the model sees escaped JSON literals on every reload.
332
+ const alreadyStringified = JSON.stringify({ temp: "22C", condition: "sunny" });
333
+
334
+ const conversationData: ConversationData = {
335
+ messages: [
336
+ {
337
+ role: "user",
338
+ toolResults: [{ id: "call_1", result: alreadyStringified }],
339
+ },
340
+ ],
341
+ };
342
+
343
+ agent.loadConversationSpy.and.returnValue(Promise.resolve(conversationData));
344
+
345
+ const input: any = { conversationId: "conv-123" };
346
+ await agent.testBeforeRun(input, {});
347
+
348
+ expect(input.history[0]).toEqual({
349
+ role: "tool",
350
+ toolCallId: "call_1",
351
+ toolName: "",
352
+ result: alreadyStringified, // not JSON.stringify(alreadyStringified)
353
+ });
354
+ });
355
+
356
+ it("should preserve assistant messages with toolCalls", async () => {
357
+ const conversationData: ConversationData = {
358
+ messages: [
359
+ {
360
+ role: "assistant",
361
+ content: "Let me check",
362
+ toolCalls: [
363
+ { id: "call_1", name: "get-weather", input: { city: "Stockholm" } },
364
+ ],
365
+ },
366
+ ],
367
+ };
368
+
369
+ agent.loadConversationSpy.and.returnValue(Promise.resolve(conversationData));
370
+
371
+ const input: any = { conversationId: "conv-123" };
372
+ await agent.testBeforeRun(input, {});
373
+
374
+ expect(input.history).toBeDefined();
375
+ expect(input.history.length).toBe(1);
376
+ expect(input.history[0]).toEqual({
377
+ role: "assistant",
378
+ content: "Let me check",
379
+ toolCalls: [
380
+ { id: "call_1", name: "get-weather", input: { city: "Stockholm" } },
381
+ ],
382
+ });
383
+ });
384
+ });
385
+
386
+ describe("message conversion", () => {
387
+ it("should convert text content blocks to StorageMessage format", () => {
388
+ const llmMessages = [
389
+ {
390
+ role: "user",
391
+ content: [{ type: "text", text: "Hello world" }],
392
+ },
393
+ {
394
+ role: "assistant",
395
+ content: [{ type: "text", text: "Hi there!" }],
396
+ },
397
+ ];
398
+
399
+ const result = agent.testConvertToMessages(llmMessages);
400
+
401
+ expect(result).toEqual([
402
+ { role: "user", content: "Hello world" },
403
+ { role: "assistant", content: "Hi there!" },
404
+ ]);
405
+ });
406
+
407
+ it("should convert tool calls from content blocks", () => {
408
+ const llmMessages = [
409
+ {
410
+ role: "assistant",
411
+ content: [
412
+ {
413
+ type: "tool_use",
414
+ id: "tool-123",
415
+ name: "search-cars",
416
+ input: { query: "SUV" },
417
+ },
418
+ ],
419
+ },
420
+ ];
421
+
422
+ const result = agent.testConvertToMessages(llmMessages);
423
+
424
+ expect(result).toEqual([
425
+ {
426
+ role: "assistant",
427
+ toolCalls: [
428
+ {
429
+ id: "tool-123",
430
+ name: "search-cars",
431
+ input: { query: "SUV" },
432
+ },
433
+ ],
434
+ },
435
+ ]);
436
+ });
437
+
438
+ it("should convert tool results from content blocks", () => {
439
+ const llmMessages = [
440
+ {
441
+ role: "user",
442
+ content: [
443
+ {
444
+ type: "tool_result",
445
+ tool_use_id: "tool-123",
446
+ content: { cars: ["Model X", "Model Y"] },
447
+ },
448
+ ],
449
+ },
450
+ ];
451
+
452
+ const result = agent.testConvertToMessages(llmMessages);
453
+
454
+ expect(result).toEqual([
455
+ {
456
+ role: "user",
457
+ toolResults: [
458
+ {
459
+ id: "tool-123",
460
+ result: { cars: ["Model X", "Model Y"] },
461
+ },
462
+ ],
463
+ },
464
+ ]);
465
+ });
466
+
467
+ it("should handle string content (legacy format)", () => {
468
+ const llmMessages = [
469
+ {
470
+ role: "user",
471
+ content: "Hello",
472
+ },
473
+ {
474
+ role: "assistant",
475
+ content: "Hi there",
476
+ },
477
+ ];
478
+
479
+ const result = agent.testConvertToMessages(llmMessages);
480
+
481
+ expect(result).toEqual([
482
+ { role: "user", content: "Hello" },
483
+ { role: "assistant", content: "Hi there" },
484
+ ]);
485
+ });
486
+
487
+ it("should handle mixed content blocks (text + tool calls)", () => {
488
+ const llmMessages = [
489
+ {
490
+ role: "assistant",
491
+ content: [
492
+ { type: "text", text: "Let me search for that" },
493
+ {
494
+ type: "tool_use",
495
+ id: "tool-123",
496
+ name: "search-cars",
497
+ input: { query: "sedan" },
498
+ },
499
+ ],
500
+ },
501
+ ];
502
+
503
+ const result = agent.testConvertToMessages(llmMessages);
504
+
505
+ expect(result).toEqual([
506
+ {
507
+ role: "assistant",
508
+ content: "Let me search for that",
509
+ toolCalls: [
510
+ {
511
+ id: "tool-123",
512
+ name: "search-cars",
513
+ input: { query: "sedan" },
514
+ },
515
+ ],
516
+ },
517
+ ]);
518
+ });
519
+ });
520
+ });
@@ -0,0 +1,144 @@
1
+ import { InMemoryConversationAgent, FlinkContext } from "../../src";
2
+
3
+ class TestAgent extends InMemoryConversationAgent<FlinkContext> {
4
+ id = "test-agent";
5
+ description = "Test agent";
6
+ instructions() { return "You are a test agent"; }
7
+ tools = [];
8
+ }
9
+
10
+ describe("InMemoryConversationAgent", () => {
11
+ let agent: TestAgent;
12
+
13
+ beforeEach(() => {
14
+ InMemoryConversationAgent.clearAll();
15
+ agent = new TestAgent();
16
+ (agent as any).ctx = {} as FlinkContext;
17
+ });
18
+
19
+ describe("conversation storage", () => {
20
+ it("should save and load conversations", async () => {
21
+ const conversationId = "test-conv-1";
22
+ const testData = {
23
+ messages: [
24
+ { role: "user" as const, content: "Hello" },
25
+ { role: "assistant" as const, content: "Hi there" },
26
+ ],
27
+ providerMetadata: { openai: { responseId: "resp-123" } },
28
+ };
29
+
30
+ // Save
31
+ await (agent as any).saveConversation(conversationId, testData, {}, {});
32
+
33
+ // Load
34
+ const loaded = await (agent as any).loadConversation(conversationId);
35
+
36
+ expect(loaded).toEqual(testData);
37
+ expect(loaded?.messages.length).toBe(2);
38
+ expect(loaded?.providerMetadata?.openai?.responseId).toBe("resp-123");
39
+ });
40
+
41
+ it("should return null for non-existent conversations", async () => {
42
+ const loaded = await (agent as any).loadConversation("non-existent");
43
+ expect(loaded).toBeNull();
44
+ });
45
+
46
+ it("should update existing conversations", async () => {
47
+ const conversationId = "test-conv-2";
48
+
49
+ // First save
50
+ await (agent as any).saveConversation(
51
+ conversationId,
52
+ { messages: [{ role: "user", content: "First" }] },
53
+ {},
54
+ {}
55
+ );
56
+
57
+ // Update
58
+ await (agent as any).saveConversation(
59
+ conversationId,
60
+ {
61
+ messages: [
62
+ { role: "user", content: "First" },
63
+ { role: "assistant", content: "Second" },
64
+ ],
65
+ },
66
+ {},
67
+ {}
68
+ );
69
+
70
+ const loaded = await (agent as any).loadConversation(conversationId);
71
+ expect(loaded?.messages.length).toBe(2);
72
+ });
73
+ });
74
+
75
+ describe("static utilities", () => {
76
+ it("should track conversation count", async () => {
77
+ expect(InMemoryConversationAgent.getConversationCount()).toBe(0);
78
+
79
+ await (agent as any).saveConversation("conv-1", { messages: [] }, {}, {});
80
+ expect(InMemoryConversationAgent.getConversationCount()).toBe(1);
81
+
82
+ await (agent as any).saveConversation("conv-2", { messages: [] }, {}, {});
83
+ expect(InMemoryConversationAgent.getConversationCount()).toBe(2);
84
+ });
85
+
86
+ it("should clear all conversations", async () => {
87
+ await (agent as any).saveConversation("conv-1", { messages: [] }, {}, {});
88
+ await (agent as any).saveConversation("conv-2", { messages: [] }, {}, {});
89
+
90
+ expect(InMemoryConversationAgent.getConversationCount()).toBe(2);
91
+
92
+ InMemoryConversationAgent.clearAll();
93
+
94
+ expect(InMemoryConversationAgent.getConversationCount()).toBe(0);
95
+ });
96
+
97
+ it("should get specific conversation", async () => {
98
+ const testData = { messages: [{ role: "user" as const, content: "Test" }] };
99
+ await (agent as any).saveConversation("conv-1", testData, {}, {});
100
+
101
+ const retrieved = InMemoryConversationAgent.getConversation("conv-1");
102
+ expect(retrieved).toEqual(testData);
103
+
104
+ const missing = InMemoryConversationAgent.getConversation("missing");
105
+ expect(missing).toBeUndefined();
106
+ });
107
+
108
+ it("should get all conversation IDs", async () => {
109
+ await (agent as any).saveConversation("conv-1", { messages: [] }, {}, {});
110
+ await (agent as any).saveConversation("conv-2", { messages: [] }, {}, {});
111
+ await (agent as any).saveConversation("conv-3", { messages: [] }, {}, {});
112
+
113
+ const ids = InMemoryConversationAgent.getAllConversationIds();
114
+ expect(ids.length).toBe(3);
115
+ expect(ids).toContain("conv-1");
116
+ expect(ids).toContain("conv-2");
117
+ expect(ids).toContain("conv-3");
118
+ });
119
+ });
120
+
121
+ describe("isolation", () => {
122
+ it("should isolate conversations by ID", async () => {
123
+ await (agent as any).saveConversation(
124
+ "conv-a",
125
+ { messages: [{ role: "user" as const, content: "A" }] },
126
+ {},
127
+ {}
128
+ );
129
+
130
+ await (agent as any).saveConversation(
131
+ "conv-b",
132
+ { messages: [{ role: "user" as const, content: "B" }] },
133
+ {},
134
+ {}
135
+ );
136
+
137
+ const convA = await (agent as any).loadConversation("conv-a");
138
+ const convB = await (agent as any).loadConversation("conv-b");
139
+
140
+ expect(convA?.messages[0].content).toBe("A");
141
+ expect(convB?.messages[0].content).toBe("B");
142
+ });
143
+ });
144
+ });