@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,413 @@
1
+ import { FlinkContext } from "../FlinkContext";
2
+ import { FlinkLogFactory } from "../FlinkLogFactory";
3
+ import { AgentExecuteContext, AgentExecuteInput, AgentExecuteResult, AgentFinishContext, FlinkAgent, Message } from "./FlinkAgent";
4
+
5
+ export const logger = FlinkLogFactory.createLogger("flink.ai.conversation-agent");
6
+
7
+ /**
8
+ * Storage message format - flexible structure for conversation persistence.
9
+ *
10
+ * Supports text content, tool calls, and tool results in a single message.
11
+ * More flexible than FlinkAgent's union Message type for storage purposes.
12
+ */
13
+ export interface StorageMessage {
14
+ role: "user" | "assistant" | "system" | "tool";
15
+ content?: string;
16
+ toolCalls?: Array<{
17
+ id: string;
18
+ name: string;
19
+ input: any;
20
+ }>;
21
+ toolResults?: Array<{
22
+ id: string;
23
+ result: any;
24
+ }>;
25
+ }
26
+
27
+ /**
28
+ * Conversation data structure for persistent storage.
29
+ *
30
+ * @property messages - Array of conversation messages in the storage format
31
+ * @property providerMetadata - Provider-specific metadata (e.g., OpenAI's responseId, instructionsHash)
32
+ */
33
+ export interface ConversationData {
34
+ messages: StorageMessage[];
35
+ providerMetadata?: Record<string, any>;
36
+ }
37
+
38
+ /**
39
+ * Abstract base class for agents with automatic conversation persistence.
40
+ *
41
+ * ConversationAgent eliminates boilerplate by automatically:
42
+ * - Loading conversation history before agent execution
43
+ * - Saving messages and provider metadata after execution
44
+ * - Enabling LLM provider optimizations (e.g., OpenAI's previousResponseId for 40-80% cost savings)
45
+ *
46
+ * ## Benefits
47
+ *
48
+ * - **Type Safety**: Abstract methods enforce consistent storage implementation
49
+ * - **Backend Flexibility**: Works with MongoDB, Redis, in-memory, or any storage backend
50
+ * - **Sub-Agent Aware**: Automatically skips persistence for sub-agent calls (parent handles it)
51
+ * - **Metadata Tracking**: Preserves provider metadata for debugging and future extensibility
52
+ *
53
+ * ## Usage Examples
54
+ *
55
+ * ### MongoDB Storage
56
+ * ```typescript
57
+ * export default class CarAgent extends ConversationAgent<AppContext> {
58
+ * id = "car-agent";
59
+ * description = "Car assistant with persistent conversations";
60
+ * instructions = "You are a helpful car expert...";
61
+ * tools = ["search-cars"];
62
+ *
63
+ * protected async loadConversation(conversationId: string) {
64
+ * const conv = await this.ctx.repos.conversationRepo.getById(conversationId);
65
+ * return conv ? {
66
+ * messages: conv.messages,
67
+ * providerMetadata: conv.providerMetadata,
68
+ * } : null;
69
+ * }
70
+ *
71
+ * protected async saveConversation(
72
+ * conversationId: string,
73
+ * data: ConversationData,
74
+ * result: AgentExecuteResult,
75
+ * context: AgentFinishContext
76
+ * ) {
77
+ * await this.ctx.repos.conversationRepo.upsert({
78
+ * _id: conversationId,
79
+ * agentId: this.id,
80
+ * userId: context.user?.id,
81
+ * messages: data.messages,
82
+ * providerMetadata: data.providerMetadata,
83
+ * updatedAt: new Date(),
84
+ * });
85
+ * }
86
+ * }
87
+ * ```
88
+ *
89
+ * ### Redis Storage
90
+ * ```typescript
91
+ * export default class CarAgent extends ConversationAgent<AppContext> {
92
+ * id = "car-agent";
93
+ * description = "Car assistant with Redis-backed conversations";
94
+ * instructions = "You are a helpful car expert...";
95
+ * tools = ["search-cars"];
96
+ *
97
+ * protected async loadConversation(conversationId: string) {
98
+ * const key = `conversation:${conversationId}`;
99
+ * const data = await this.ctx.redis.get(key);
100
+ * return data ? JSON.parse(data) : null;
101
+ * }
102
+ *
103
+ * protected async saveConversation(
104
+ * conversationId: string,
105
+ * data: ConversationData
106
+ * ) {
107
+ * const key = `conversation:${conversationId}`;
108
+ * await this.ctx.redis.setex(
109
+ * key,
110
+ * 86400, // 24 hour TTL
111
+ * JSON.stringify(data)
112
+ * );
113
+ * }
114
+ * }
115
+ * ```
116
+ *
117
+ * ### In-Memory Storage (Development/Testing)
118
+ * ```typescript
119
+ * const conversationStore = new Map<string, ConversationData>();
120
+ *
121
+ * export default class CarAgent extends ConversationAgent<AppContext> {
122
+ * id = "car-agent";
123
+ * description = "Car assistant with in-memory conversations";
124
+ * instructions = "You are a helpful car expert...";
125
+ * tools = ["search-cars"];
126
+ *
127
+ * protected async loadConversation(conversationId: string) {
128
+ * return conversationStore.get(conversationId) || null;
129
+ * }
130
+ *
131
+ * protected async saveConversation(
132
+ * conversationId: string,
133
+ * data: ConversationData
134
+ * ) {
135
+ * conversationStore.set(conversationId, data);
136
+ * }
137
+ * }
138
+ * ```
139
+ *
140
+ * ## Handler Usage
141
+ *
142
+ * ```typescript
143
+ * const handler: PostHandler<AppContext, Input, Output> = async ({ ctx, body, req }) => {
144
+ * const conversationId = body.conversationId || generateId();
145
+ *
146
+ * const result = await ctx.agents.carAgent
147
+ * .withUser(req.user)
148
+ * .execute({ message: body.message, conversationId });
149
+ *
150
+ * return {
151
+ * conversationId,
152
+ * message: (await result.result).message,
153
+ * };
154
+ * };
155
+ * ```
156
+ *
157
+ * ## Provider Metadata Flow
158
+ *
159
+ * 1. First turn: Agent executes, LLM returns metadata (e.g., `{ openai: { responseId: "..." } }`)
160
+ * 2. saveConversation() stores messages + metadata
161
+ * 3. Second turn: loadConversation() returns messages + metadata
162
+ * 4. beforeRun() populates input.providerMetadata (always preserved)
163
+ * 5. LLM adapter receives metadata for tracking/debugging purposes
164
+ * 6. Full conversation history is always sent to ensure reliability
165
+ *
166
+ * @template Context - Application context type extending FlinkContext
167
+ *
168
+ * @see {@link https://flink.dev/docs/ai/agents/persistence | Persistence Guide}
169
+ * @see {@link https://platform.openai.com/docs/guides/conversation-history | OpenAI Conversation History}
170
+ */
171
+ export abstract class ConversationAgent<Context extends FlinkContext = FlinkContext> extends FlinkAgent<Context> {
172
+ /**
173
+ * Load conversation history and provider metadata for the given conversation ID.
174
+ *
175
+ * Called automatically in beforeRun() before agent execution.
176
+ *
177
+ * @param conversationId - Unique identifier for the conversation
178
+ * @returns Conversation data (messages + providerMetadata), or null if not found
179
+ *
180
+ * @example
181
+ * ```typescript
182
+ * protected async loadConversation(conversationId: string) {
183
+ * const conv = await this.ctx.repos.conversationRepo.getById(conversationId);
184
+ * return conv ? {
185
+ * messages: conv.messages,
186
+ * providerMetadata: conv.providerMetadata,
187
+ * } : null;
188
+ * }
189
+ * ```
190
+ */
191
+ protected abstract loadConversation(conversationId: string): Promise<ConversationData | null>;
192
+
193
+ /**
194
+ * Save conversation history and provider metadata.
195
+ *
196
+ * Called automatically in afterRun() after agent execution completes successfully.
197
+ *
198
+ * @param conversationId - Unique identifier for the conversation
199
+ * @param data - Conversation data to save (messages + providerMetadata)
200
+ * @param result - Agent execution result (for access to final output if needed)
201
+ * @param context - Agent finish context (includes user, isSubAgent, etc.)
202
+ *
203
+ * @example
204
+ * ```typescript
205
+ * protected async saveConversation(
206
+ * conversationId: string,
207
+ * data: ConversationData,
208
+ * result: AgentExecuteResult,
209
+ * context: AgentFinishContext
210
+ * ) {
211
+ * await this.ctx.repos.conversationRepo.upsert({
212
+ * _id: conversationId,
213
+ * agentId: this.id,
214
+ * userId: context.user?.id,
215
+ * messages: data.messages,
216
+ * providerMetadata: data.providerMetadata,
217
+ * updatedAt: new Date(),
218
+ * });
219
+ * }
220
+ * ```
221
+ */
222
+ protected abstract saveConversation(conversationId: string, data: ConversationData, result: AgentExecuteResult, context: AgentFinishContext): Promise<void>;
223
+
224
+ /**
225
+ * Lifecycle hook: Load conversation history before agent execution.
226
+ *
227
+ * Automatically populates input.history and input.providerMetadata from storage.
228
+ *
229
+ * @internal
230
+ */
231
+ protected async beforeRun(input: AgentExecuteInput, context: AgentExecuteContext): Promise<void> {
232
+ const conversationId = input.conversationId;
233
+
234
+ if (!conversationId) {
235
+ logger.debug(`[${this.id}] No conversationId provided, starting fresh conversation`);
236
+ return;
237
+ }
238
+
239
+ try {
240
+ const conversationData = await this.loadConversation(conversationId);
241
+
242
+ if (conversationData) {
243
+ input.history = this.convertToAgentMessages(conversationData.messages);
244
+
245
+ // Always preserve provider metadata (for tracking and future use)
246
+ input.providerMetadata = conversationData.providerMetadata;
247
+
248
+ logger.debug(`[${this.id}] Loaded conversation ${conversationId}: ${conversationData.messages.length} messages`);
249
+ } else {
250
+ logger.debug(`[${this.id}] No conversation found for ${conversationId}, starting fresh`);
251
+ }
252
+ } catch (error) {
253
+ logger.error(`[${this.id}] Failed to load conversation ${conversationId}:`, error);
254
+ throw error; // Propagate load errors
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Lifecycle hook: Save conversation history after agent execution.
260
+ *
261
+ * Automatically converts LLM messages to storage format and saves with provider metadata.
262
+ * Skips save for sub-agent calls (parent agent handles persistence).
263
+ *
264
+ * @internal
265
+ */
266
+ protected async afterRun(result: AgentExecuteResult, context: AgentFinishContext): Promise<void> {
267
+ const conversationId = context.conversationId;
268
+
269
+ if (!conversationId) {
270
+ logger.debug(`[${this.id}] No conversationId provided, skipping save`);
271
+ return;
272
+ }
273
+
274
+ try {
275
+ const messages = this.convertToMessages(context.messages);
276
+ const providerMetadata = result.providerMetadata;
277
+
278
+ const conversationData: ConversationData = {
279
+ messages,
280
+ providerMetadata,
281
+ };
282
+
283
+ await this.saveConversation(conversationId, conversationData, result, context);
284
+
285
+ logger.debug(`[${this.id}] Saved conversation ${conversationId}: ${messages.length} messages, metadata=${JSON.stringify(providerMetadata)}`);
286
+ } catch (error) {
287
+ // Log but don't throw - request already succeeded, don't fail it now
288
+ logger.error(`[${this.id}] Failed to save conversation ${conversationId}:`, error);
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Convert storage messages back to agent Message format for input.history.
294
+ *
295
+ * @param storageMessages - Messages from storage in StorageMessage format
296
+ * @returns Message array compatible with AgentExecuteInput.history
297
+ *
298
+ * @internal
299
+ */
300
+ private convertToAgentMessages(storageMessages: StorageMessage[]): Message[] {
301
+ return storageMessages.flatMap((msg): Message[] => {
302
+ // Check for tool results first (can be on user or tool role messages)
303
+ if (msg.toolResults && msg.toolResults.length > 0) {
304
+ // Tool results stored in storage format, convert to agent format
305
+ // Each tool result becomes a separate Message
306
+ return msg.toolResults.map(
307
+ (toolResult): Message => ({
308
+ role: "tool",
309
+ toolCallId: toolResult.id,
310
+ toolName: "", // Not stored, but required by type
311
+ // Tool results are persisted as already-stringified payloads (see formatResultForAI).
312
+ // JSON.stringify-ing again would double-encode strings into escaped JSON literals.
313
+ result: typeof toolResult.result === "string" ? toolResult.result : JSON.stringify(toolResult.result),
314
+ })
315
+ );
316
+ } else if (msg.role === "user") {
317
+ return [{ role: "user", content: msg.content || "" }];
318
+ } else if (msg.role === "assistant") {
319
+ return [
320
+ {
321
+ role: "assistant",
322
+ content: msg.content || "",
323
+ toolCalls: msg.toolCalls,
324
+ },
325
+ ];
326
+ } else {
327
+ // Fallback for unexpected cases
328
+ return [{ role: "user", content: msg.content || "" }];
329
+ }
330
+ });
331
+ }
332
+
333
+ /**
334
+ * Convert LLM adapter messages to storage format for persistence.
335
+ *
336
+ * @param llmMessages - Messages from LLM adapter in their native format
337
+ * @returns StorageMessage array
338
+ *
339
+ * @internal
340
+ */
341
+ private convertToMessages(llmMessages: any[]): StorageMessage[] {
342
+ return llmMessages.map((msg) => {
343
+ const message: StorageMessage = {
344
+ role: msg.role,
345
+ };
346
+
347
+ // Handle content blocks
348
+ if (Array.isArray(msg.content)) {
349
+ const text = this.extractTextFromBlocks(msg.content);
350
+ if (text) message.content = text;
351
+
352
+ const toolCalls = this.extractToolCalls(msg.content);
353
+ if (toolCalls.length > 0) message.toolCalls = toolCalls;
354
+
355
+ const toolResults = this.extractToolResults(msg.content);
356
+ if (toolResults.length > 0) message.toolResults = toolResults;
357
+ } else if (typeof msg.content === "string") {
358
+ message.content = msg.content;
359
+ }
360
+
361
+ return message;
362
+ });
363
+ }
364
+
365
+ /**
366
+ * Extract text content from message content blocks.
367
+ *
368
+ * @param blocks - Content blocks from LLM message
369
+ * @returns Concatenated text content, or undefined if no text blocks
370
+ *
371
+ * @internal
372
+ */
373
+ private extractTextFromBlocks(blocks: any[]): string | undefined {
374
+ const textBlocks = blocks.filter((block) => block.type === "text");
375
+ if (textBlocks.length === 0) return undefined;
376
+ return textBlocks.map((block) => block.text).join("");
377
+ }
378
+
379
+ /**
380
+ * Extract tool calls from message content blocks.
381
+ *
382
+ * @param blocks - Content blocks from LLM message
383
+ * @returns Array of tool call objects
384
+ *
385
+ * @internal
386
+ */
387
+ private extractToolCalls(blocks: any[]): any[] {
388
+ return blocks
389
+ .filter((block) => block.type === "tool_use")
390
+ .map((block) => ({
391
+ id: block.id,
392
+ name: block.name,
393
+ input: block.input,
394
+ }));
395
+ }
396
+
397
+ /**
398
+ * Extract tool results from message content blocks.
399
+ *
400
+ * @param blocks - Content blocks from LLM message
401
+ * @returns Array of tool result objects
402
+ *
403
+ * @internal
404
+ */
405
+ private extractToolResults(blocks: any[]): any[] {
406
+ return blocks
407
+ .filter((block) => block.type === "tool_result")
408
+ .map((block) => ({
409
+ id: block.tool_use_id,
410
+ result: block.content,
411
+ }));
412
+ }
413
+ }