@flink-app/flink 1.0.0 → 2.0.0-alpha.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/cli/build.ts +8 -1
  3. package/cli/run.ts +8 -1
  4. package/dist/cli/build.js +8 -1
  5. package/dist/cli/run.js +8 -1
  6. package/dist/src/FlinkApp.d.ts +33 -0
  7. package/dist/src/FlinkApp.js +247 -27
  8. package/dist/src/FlinkContext.d.ts +21 -0
  9. package/dist/src/FlinkHttpHandler.d.ts +90 -1
  10. package/dist/src/TypeScriptCompiler.d.ts +42 -0
  11. package/dist/src/TypeScriptCompiler.js +346 -4
  12. package/dist/src/TypeScriptUtils.js +4 -0
  13. package/dist/src/ai/AgentRunner.d.ts +39 -0
  14. package/dist/src/ai/AgentRunner.js +625 -0
  15. package/dist/src/ai/FlinkAgent.d.ts +446 -0
  16. package/dist/src/ai/FlinkAgent.js +633 -0
  17. package/dist/src/ai/FlinkTool.d.ts +37 -0
  18. package/dist/src/ai/FlinkTool.js +2 -0
  19. package/dist/src/ai/LLMAdapter.d.ts +119 -0
  20. package/dist/src/ai/LLMAdapter.js +2 -0
  21. package/dist/src/ai/SubAgentExecutor.d.ts +36 -0
  22. package/dist/src/ai/SubAgentExecutor.js +220 -0
  23. package/dist/src/ai/ToolExecutor.d.ts +35 -0
  24. package/dist/src/ai/ToolExecutor.js +237 -0
  25. package/dist/src/ai/index.d.ts +5 -0
  26. package/dist/src/ai/index.js +21 -0
  27. package/dist/src/handlers/StreamWriterFactory.d.ts +20 -0
  28. package/dist/src/handlers/StreamWriterFactory.js +83 -0
  29. package/dist/src/index.d.ts +4 -0
  30. package/dist/src/index.js +4 -0
  31. package/dist/src/utils.d.ts +30 -0
  32. package/dist/src/utils.js +52 -0
  33. package/package.json +14 -2
  34. package/readme.md +425 -0
  35. package/spec/AgentDuplicateDetection.spec.ts +112 -0
  36. package/spec/AgentRunner.spec.ts +527 -0
  37. package/spec/ConversationHooks.spec.ts +290 -0
  38. package/spec/FlinkAgent.spec.ts +310 -0
  39. package/spec/FlinkApp.onError.spec.ts +1 -2
  40. package/spec/StreamingIntegration.spec.ts +138 -0
  41. package/spec/SubAgentSupport.spec.ts +941 -0
  42. package/spec/ToolExecutor.spec.ts +360 -0
  43. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar.js +57 -0
  44. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar2.js +59 -0
  45. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema.js +53 -0
  46. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema2.js +53 -0
  47. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema3.js +53 -0
  48. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema.js +55 -0
  49. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema2.js +55 -0
  50. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile.js +58 -0
  51. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile2.js +58 -0
  52. package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler.js +53 -0
  53. package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler2.js +55 -0
  54. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchCar.js +58 -0
  55. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOnboardingSession.js +76 -0
  56. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOrderWithComplexTypes.js +58 -0
  57. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchProductWithIntersection.js +59 -0
  58. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchUserWithUnion.js +59 -0
  59. package/spec/mock-project/dist/spec/mock-project/src/handlers/PostCar.js +55 -0
  60. package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogin.js +56 -0
  61. package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogout.js +55 -0
  62. package/spec/mock-project/dist/spec/mock-project/src/handlers/PutCar.js +55 -0
  63. package/spec/mock-project/dist/spec/mock-project/src/index.js +83 -0
  64. package/spec/mock-project/dist/spec/mock-project/src/repos/CarRepo.js +26 -0
  65. package/spec/mock-project/dist/spec/mock-project/src/schemas/Car.js +2 -0
  66. package/spec/mock-project/dist/spec/mock-project/src/schemas/DefaultExportSchema.js +2 -0
  67. package/spec/mock-project/dist/spec/mock-project/src/schemas/FileWithTwoSchemas.js +2 -0
  68. package/spec/mock-project/dist/src/FlinkApp.js +1012 -0
  69. package/spec/mock-project/dist/src/FlinkContext.js +2 -0
  70. package/spec/mock-project/dist/src/FlinkErrors.js +143 -0
  71. package/spec/mock-project/dist/src/FlinkHttpHandler.js +47 -0
  72. package/spec/mock-project/dist/src/FlinkJob.js +2 -0
  73. package/spec/mock-project/dist/src/FlinkLog.js +26 -0
  74. package/spec/mock-project/dist/src/FlinkPlugin.js +2 -0
  75. package/spec/mock-project/dist/src/FlinkRepo.js +224 -0
  76. package/spec/mock-project/dist/src/FlinkResponse.js +2 -0
  77. package/spec/mock-project/dist/src/ai/AgentExecutor.js +279 -0
  78. package/spec/mock-project/dist/src/ai/AgentRunner.js +625 -0
  79. package/spec/mock-project/dist/src/ai/FlinkAgent.js +633 -0
  80. package/spec/mock-project/dist/src/ai/FlinkTool.js +2 -0
  81. package/spec/mock-project/dist/src/ai/LLMAdapter.js +2 -0
  82. package/spec/mock-project/dist/src/ai/SubAgentExecutor.js +220 -0
  83. package/spec/mock-project/dist/src/ai/ToolExecutor.js +237 -0
  84. package/spec/mock-project/dist/src/auth/FlinkAuthPlugin.js +2 -0
  85. package/spec/mock-project/dist/src/auth/FlinkAuthUser.js +2 -0
  86. package/spec/mock-project/dist/src/handlers/StreamWriterFactory.js +83 -0
  87. package/spec/mock-project/dist/src/index.js +17 -69
  88. package/spec/mock-project/dist/src/mock-data-generator.js +9 -0
  89. package/spec/mock-project/dist/src/utils.js +290 -0
  90. package/spec/mock-project/tsconfig.json +6 -1
  91. package/spec/testHelpers.ts +49 -0
  92. package/spec/utils.caseConversion.spec.ts +80 -0
  93. package/spec/utils.spec.ts +13 -13
  94. package/src/FlinkApp.ts +251 -7
  95. package/src/FlinkContext.ts +22 -0
  96. package/src/FlinkHttpHandler.ts +100 -2
  97. package/src/TypeScriptCompiler.ts +398 -7
  98. package/src/TypeScriptUtils.ts +5 -0
  99. package/src/ai/AgentRunner.ts +549 -0
  100. package/src/ai/FlinkAgent.ts +770 -0
  101. package/src/ai/FlinkTool.ts +40 -0
  102. package/src/ai/LLMAdapter.ts +96 -0
  103. package/src/ai/SubAgentExecutor.ts +199 -0
  104. package/src/ai/ToolExecutor.ts +193 -0
  105. package/src/ai/index.ts +5 -0
  106. package/src/handlers/StreamWriterFactory.ts +84 -0
  107. package/src/index.ts +4 -0
  108. package/src/utils.ts +52 -0
  109. package/tsconfig.json +6 -1
@@ -69,13 +69,118 @@ class TypeScriptCompiler {
69
69
  this.project = new Project({
70
70
  tsConfigFilePath: tsConfigPath,
71
71
  compilerOptions,
72
+ skipAddingFilesFromTsConfig: true, // Don't auto-load all files - we'll add specific directories we need
72
73
  });
73
74
 
75
+ // Load Flink-specific source paths from tsconfig.json if available
76
+ const additionalSourcePaths = this.getFlinkSourcePaths(tsConfigPath);
77
+
78
+ // Only add source files from directories we actually need to process
79
+ // This prevents loading all files from tsconfig include patterns and reduces memory usage
80
+ const defaultSourcePaths = [
81
+ join(cwd, "src/handlers/**/*.ts"),
82
+ join(cwd, "src/repos/**/*.ts"),
83
+ join(cwd, "src/tools/**/*.ts"),
84
+ join(cwd, "src/agents/**/*.ts"),
85
+ join(cwd, "src/jobs/**/*.ts"),
86
+ join(cwd, "src/index.ts"),
87
+ join(cwd, "src/schemas/**/*.ts"), // Include schemas for type resolution
88
+ ];
89
+
90
+ const allSourcePaths = [...defaultSourcePaths, ...additionalSourcePaths];
91
+ this.project.addSourceFilesAtPaths(allSourcePaths);
92
+
93
+ // Resolve imports: add any files imported by our source files
94
+ // This ensures schemas that import types from other locations work correctly
95
+ this.resolveImportedFiles();
96
+
74
97
  console.log("Loaded", this.project.getSourceFiles().length, "source file(s) from", cwd);
75
98
  console.log("Module system:", this.isEsm ? "ESM" : "CommonJS");
76
99
  console.log("Using module:", compilerOptions.module === ts.ModuleKind.ESNext ? "ESNext" : "CommonJS");
77
100
  }
78
101
 
102
+ /**
103
+ * Loads additional source paths from tsconfig.json's flink configuration.
104
+ * Allows projects to specify extra directories to include in compilation.
105
+ *
106
+ * Example tsconfig.json:
107
+ * {
108
+ * "flink": {
109
+ * "sourcePaths": ["src/custom-types/**\/*.ts", "src/utils/**\/*.ts"]
110
+ * }
111
+ * }
112
+ */
113
+ private getFlinkSourcePaths(tsConfigPath: string): string[] {
114
+ try {
115
+ if (fs.existsSync(tsConfigPath)) {
116
+ const tsConfigContent = fs.readFileSync(tsConfigPath, "utf8");
117
+ const tsConfig = JSON.parse(tsConfigContent);
118
+
119
+ if (tsConfig.flink && Array.isArray(tsConfig.flink.sourcePaths)) {
120
+ console.log("Found Flink-specific source paths:", tsConfig.flink.sourcePaths);
121
+ return tsConfig.flink.sourcePaths.map((path: string) => join(this.cwd, path));
122
+ }
123
+ }
124
+ } catch (error) {
125
+ console.warn("Error reading Flink source paths from tsconfig.json:", error);
126
+ }
127
+
128
+ return [];
129
+ }
130
+
131
+ /**
132
+ * Recursively resolves and adds imported files to the project.
133
+ * This ensures that files imported by handlers, schemas, etc. are available for type resolution.
134
+ *
135
+ * Handles three types of imports:
136
+ * 1. Relative imports (./foo, ../bar) - always resolved
137
+ * 2. Workspace package imports (@mycompany/shared) - resolved if symlinked outside node_modules
138
+ * 3. External packages (lodash, express) - skipped to avoid loading entire dependency trees
139
+ */
140
+ private resolveImportedFiles() {
141
+ const processedFiles = new Set<string>();
142
+ const filesToProcess = [...this.project.getSourceFiles()];
143
+
144
+ while (filesToProcess.length > 0) {
145
+ const sourceFile = filesToProcess.pop()!;
146
+ const filePath = sourceFile.getFilePath();
147
+
148
+ if (processedFiles.has(filePath)) {
149
+ continue;
150
+ }
151
+
152
+ processedFiles.add(filePath);
153
+
154
+ // Get all import declarations
155
+ const importDeclarations = sourceFile.getImportDeclarations();
156
+
157
+ for (const importDecl of importDeclarations) {
158
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
159
+
160
+ // Try to resolve the imported file
161
+ const moduleSourceFile = importDecl.getModuleSpecifierSourceFile();
162
+
163
+ if (moduleSourceFile) {
164
+ const importedPath = moduleSourceFile.getFilePath();
165
+
166
+ // For relative imports (./foo, ../bar), always include
167
+ const isRelativeImport = moduleSpecifier.startsWith(".") || moduleSpecifier.startsWith("/");
168
+
169
+ // For package imports (@foo/bar, foo), check if resolved path is outside node_modules
170
+ // This handles workspace packages that are symlinked (pnpm, yarn workspaces)
171
+ const isWorkspacePackage = !isRelativeImport && !importedPath.includes("node_modules");
172
+
173
+ // Skip if it's in node_modules (external package) or already processed
174
+ if ((isRelativeImport || isWorkspacePackage) && !processedFiles.has(importedPath)) {
175
+ filesToProcess.push(moduleSourceFile);
176
+ }
177
+ }
178
+ }
179
+ }
180
+
181
+ console.log("Resolved imports, total files loaded:", processedFiles.size);
182
+ }
183
+
79
184
  /**
80
185
  * Detects if the project is using ESM (ECMAScript Modules)
81
186
  * by checking type in package.json.
@@ -171,6 +276,54 @@ class TypeScriptCompiler {
171
276
  return true;
172
277
  }
173
278
 
279
+ /**
280
+ * Finds a variable declaration by its TypeScript type name.
281
+ * This allows finding variables based on their type annotation rather than variable name.
282
+ *
283
+ * @param sf Source file to search in
284
+ * @param typeName Name of the type to search for (e.g., "FlinkToolProps", "FlinkAgentProps")
285
+ * @returns The first matching variable declaration, or undefined if not found
286
+ */
287
+ private findVariableDeclarationByType(sf: SourceFile, typeName: string) {
288
+ // Get all variable declarations from the source file
289
+ const variableDeclarations = sf.getVariableDeclarations();
290
+
291
+ for (const varDecl of variableDeclarations) {
292
+ // Check if the variable is exported
293
+ const variableStatement = varDecl.getVariableStatement();
294
+ if (!variableStatement?.isExported()) {
295
+ continue;
296
+ }
297
+
298
+ // Check if it's a const declaration
299
+ if (varDecl.getVariableStatement()?.getDeclarationKind() !== VariableDeclarationKind.Const) {
300
+ continue;
301
+ }
302
+
303
+ // Get the type node (explicit type annotation)
304
+ const typeNode = varDecl.getTypeNode();
305
+ if (!typeNode) {
306
+ continue;
307
+ }
308
+
309
+ // Check if it's a type reference
310
+ if (typeNode.getKind() === SyntaxKind.TypeReference) {
311
+ const typeRef = typeNode as TypeReferenceNode;
312
+ const typeRefName = typeRef.getTypeName();
313
+
314
+ // Handle both simple type references (FlinkToolProps) and qualified names (flink.FlinkToolProps)
315
+ const typeRefText = typeRefName.getText();
316
+
317
+ // Check if the type name matches (either exact match or ends with the type name for qualified references)
318
+ if (typeRefText === typeName || typeRefText.endsWith(`.${typeName}`)) {
319
+ return varDecl;
320
+ }
321
+ }
322
+ }
323
+
324
+ return undefined;
325
+ }
326
+
174
327
  /**
175
328
  * Scans project for handlers and add those to Flink
176
329
  * "singleton" property `autoRegisteredHandlers` so they can
@@ -338,6 +491,243 @@ autoRegisteredHandlers.push(...handlers);
338
491
  return generatedFile;
339
492
  }
340
493
 
494
+ /**
495
+ * Scans project for tools and adds those to Flink
496
+ * "singleton" property `autoRegisteredTools` so they can
497
+ * be registered during start.
498
+ */
499
+ async parseTools() {
500
+ const generatedFile = this.createSourceFile(
501
+ ["generatedTools.ts"],
502
+ `// Generated ${new Date()}
503
+ import { autoRegisteredTools } from "@flink-app/flink";
504
+ export const tools = [];
505
+ autoRegisteredTools.push(...tools);
506
+ `
507
+ );
508
+
509
+ const toolsArr = generatedFile.getVariableDeclarationOrThrow("tools").getFirstDescendantByKindOrThrow(SyntaxKind.ArrayLiteralExpression);
510
+
511
+ const imports: OptionalKind<ImportDeclarationStructure>[] = [];
512
+ let i = 0;
513
+
514
+ for (const sf of this.project.getSourceFiles()) {
515
+ if (!sf.getFilePath().includes("src/tools/")) {
516
+ continue;
517
+ }
518
+
519
+ console.log(`Detected tool ${sf.getBaseName()}`);
520
+
521
+ const namespaceImport = sf.getBaseNameWithoutExtension().replace(/\./g, "_") + "_" + i;
522
+
523
+ imports.push({
524
+ defaultImport: "* as " + namespaceImport,
525
+ moduleSpecifier: this.getModuleSpecifier(generatedFile, sf),
526
+ });
527
+
528
+ // Find the tool props variable (can have any name, but must be of type FlinkToolProps)
529
+ const toolPropsVar = this.findVariableDeclarationByType(sf, "FlinkToolProps");
530
+ const toolPropsExportName = toolPropsVar?.getName() || "Tool";
531
+
532
+ // Remove existing __file variable if it exists
533
+ const existingFile = sf.getVariableStatements().filter((vs) => {
534
+ const varNames = vs.getDeclarations().map((d) => d.getName());
535
+ return varNames.includes("__file");
536
+ });
537
+ existingFile.forEach((v) => v.remove());
538
+
539
+ // Append metadata to source file
540
+ sf.addVariableStatement({
541
+ declarationKind: VariableDeclarationKind.Const,
542
+ isExported: true,
543
+ declarations: [
544
+ {
545
+ name: "__file",
546
+ initializer: `"${sf.getBaseName()}"`,
547
+ },
548
+ ],
549
+ });
550
+
551
+ // Create an object that wraps the namespace and provides named access to the tool props
552
+ toolsArr.insertElement(i, `{...${namespaceImport}, Tool: ${namespaceImport}.${toolPropsExportName}}`);
553
+ i++;
554
+ }
555
+
556
+ generatedFile.addImportDeclarations(imports);
557
+ await generatedFile.save();
558
+
559
+ return generatedFile;
560
+ }
561
+
562
+ /**
563
+ * Scans project for agents and validates tool references.
564
+ * Agents are declarative (no default function).
565
+ */
566
+ async parseAgents() {
567
+ const generatedFile = this.createSourceFile(
568
+ ["generatedAgents.ts"],
569
+ `// Generated ${new Date()}
570
+ import { autoRegisteredAgents } from "@flink-app/flink";
571
+ export const agents = [];
572
+ autoRegisteredAgents.push(...agents);
573
+ `
574
+ );
575
+
576
+ const agentsArr = generatedFile.getVariableDeclarationOrThrow("agents").getFirstDescendantByKindOrThrow(SyntaxKind.ArrayLiteralExpression);
577
+
578
+ const imports: OptionalKind<ImportDeclarationStructure>[] = [];
579
+
580
+ // Collect all registered tool ids for validation
581
+ const registeredToolIds = new Set<string>();
582
+ for (const sf of this.project.getSourceFiles()) {
583
+ if (sf.getFilePath().includes("src/tools/")) {
584
+ const toolExport = this.findVariableDeclarationByType(sf, "FlinkToolProps");
585
+ if (toolExport) {
586
+ const props = toolExport.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression);
587
+ // Try 'id' first, fallback to 'name' for backwards compatibility
588
+ const idProp = props?.getProperty("id") || props?.getProperty("name");
589
+ if (idProp) {
590
+ const idValue = idProp.getLastChildByKind(SyntaxKind.StringLiteral);
591
+ if (idValue) {
592
+ registeredToolIds.add(idValue.getLiteralText());
593
+ }
594
+ }
595
+ }
596
+ }
597
+ }
598
+
599
+ let i = 0;
600
+
601
+ for (const sf of this.project.getSourceFiles()) {
602
+ if (!sf.getFilePath().includes("src/agents/")) {
603
+ continue;
604
+ }
605
+
606
+ // Check if file has class extending FlinkAgent (new pattern)
607
+ const agentClass = sf.getClasses().find((cls) => {
608
+ const baseClass = cls.getBaseClass();
609
+ return baseClass?.getName() === "FlinkAgent";
610
+ });
611
+
612
+ if (!agentClass) {
613
+ // Skip files without FlinkAgent class
614
+ continue;
615
+ }
616
+
617
+ console.log(`Detected agent ${sf.getBaseName()}`);
618
+
619
+ // Validate tool references exist (read from class properties)
620
+ const toolsProperty = agentClass.getProperty("tools");
621
+ if (toolsProperty) {
622
+ const initializer = toolsProperty.getInitializer();
623
+ if (initializer && initializer.getKind() === SyntaxKind.ArrayLiteralExpression) {
624
+ const toolsArray = initializer as any;
625
+ const toolElements = toolsArray.getElements();
626
+ for (const toolElement of toolElements) {
627
+ let toolName: string;
628
+
629
+ // Handle string literals, method calls, and identifier references (tool imports)
630
+ if (toolElement.getKind() === SyntaxKind.StringLiteral) {
631
+ // Direct string: "tool-name"
632
+ toolName = toolElement.getText().replace(/['"]/g, "");
633
+ } else if (toolElement.getKind() === SyntaxKind.CallExpression) {
634
+ // Method call: this.useTool("tool-name")
635
+ const args = toolElement.getArguments();
636
+ if (args.length > 0 && args[0].getKind() === SyntaxKind.StringLiteral) {
637
+ toolName = args[0].getText().replace(/['"]/g, "");
638
+ } else {
639
+ console.warn(`Agent ${sf.getBaseName()} has non-string tool reference, skipping validation`);
640
+ continue;
641
+ }
642
+ } else if (toolElement.getKind() === SyntaxKind.Identifier) {
643
+ // Tool file reference (imported): SearchCarsByBrandTool
644
+ // Look up the import to find the actual tool file
645
+ const importName = toolElement.getText();
646
+ const importDecl = sf.getImportDeclarations().find((imp) => {
647
+ const namedImports = imp.getNamedImports();
648
+ return namedImports.some((ni) => ni.getName() === importName);
649
+ });
650
+
651
+ if (!importDecl) {
652
+ // Try namespace import (* as Foo)
653
+ const namespaceImport = sf.getImportDeclarations().find((imp) => {
654
+ return imp.getNamespaceImport()?.getText() === importName;
655
+ });
656
+
657
+ if (namespaceImport) {
658
+ const moduleSpecifier = namespaceImport.getModuleSpecifierValue();
659
+ // Extract tool ID from the tool file path
660
+ // e.g., "../tools/SearchCarsByBrandTool" -> find in registeredToolIds
661
+ const toolFileName = moduleSpecifier.split("/").pop()?.replace(/\.ts$/, "");
662
+ const matchingTool = Array.from(registeredToolIds).find((id) => {
663
+ // Try to match by searching for the tool ID
664
+ // This is a heuristic - we'll validate it exists
665
+ return true; // Skip validation for imported tools for now
666
+ });
667
+ // Skip validation - tool imports are validated at runtime
668
+ continue;
669
+ }
670
+
671
+ console.warn(`Agent ${sf.getBaseName()} references tool "${importName}" but it's not imported, skipping validation`);
672
+ continue;
673
+ }
674
+
675
+ // Skip validation for tool file references - they're validated at runtime
676
+ continue;
677
+ } else {
678
+ console.warn(`Agent ${sf.getBaseName()} has unexpected tool reference format: ${toolElement.getText()}`);
679
+ continue;
680
+ }
681
+
682
+ if (!registeredToolIds.has(toolName)) {
683
+ console.error(`Agent ${sf.getBaseName()} references tool "${toolName}" which does not exist`);
684
+ throw new Error(`Invalid tool reference in agent ${sf.getBaseName()}`);
685
+ }
686
+ }
687
+ }
688
+ }
689
+
690
+ const className = agentClass.getName();
691
+ if (!className) {
692
+ console.error(`Agent class in ${sf.getBaseName()} has no name`);
693
+ continue;
694
+ }
695
+
696
+ imports.push({
697
+ defaultImport: className,
698
+ moduleSpecifier: this.getModuleSpecifier(generatedFile, sf),
699
+ });
700
+
701
+ // Remove existing __file variable
702
+ const existingFile = sf.getVariableStatements().filter((vs) => {
703
+ const varNames = vs.getDeclarations().map((d) => d.getName());
704
+ return varNames.includes("__file");
705
+ });
706
+ existingFile.forEach((v) => v.remove());
707
+
708
+ // Append metadata
709
+ sf.addVariableStatement({
710
+ declarationKind: VariableDeclarationKind.Const,
711
+ isExported: true,
712
+ declarations: [
713
+ {
714
+ name: "__file",
715
+ initializer: `"${sf.getBaseName()}"`,
716
+ },
717
+ ],
718
+ });
719
+
720
+ // Register the agent class
721
+ agentsArr.insertElement(i, `{ default: ${className}, __file: "${sf.getBaseName()}" }`);
722
+ i++;
723
+ }
724
+
725
+ generatedFile.addImportDeclarations(imports);
726
+ await generatedFile.save();
727
+
728
+ return generatedFile;
729
+ }
730
+
341
731
  /**
342
732
  * Generates a start script that will import references to handlers, repos and the
343
733
  * actual Flink app to start.
@@ -356,6 +746,8 @@ autoRegisteredHandlers.push(...handlers);
356
746
  `// Generated ${new Date()}
357
747
  import "./generatedHandlers${this.isEsm ? ".js" : ""}";
358
748
  import "./generatedRepos${this.isEsm ? ".js" : ""}";
749
+ import "./generatedTools${this.isEsm ? ".js" : ""}";
750
+ import "./generatedAgents${this.isEsm ? ".js" : ""}";
359
751
  import "./generatedJobs${this.isEsm ? ".js" : ""}";
360
752
  import "..${appEntryScript.replace(/\.ts/g, "")}${this.isEsm ? ".js" : ""}";
361
753
  export default {}; // Export an empty object to make it a module
@@ -417,9 +809,7 @@ export default {}; // Export an empty object to make it a module
417
809
  if (!interfaceName) return;
418
810
 
419
811
  // Check if already copied
420
- const existingInterface = this.parsedTsSchemas.find(
421
- s => s.includes(`interface ${interfaceName} `) || s.includes(`type ${interfaceName} =`)
422
- );
812
+ const existingInterface = this.parsedTsSchemas.find((s) => s.includes(`interface ${interfaceName} `) || s.includes(`type ${interfaceName} =`));
423
813
  if (existingInterface) return;
424
814
 
425
815
  // Copy the interface
@@ -458,7 +848,7 @@ export default {}; // Export an empty object to make it a module
458
848
 
459
849
  // Check for void and undefined types - these indicate no schema validation needed
460
850
  const schemaText = schema.getText();
461
- if (schemaText === 'void' || schemaText === 'undefined') {
851
+ if (schemaText === "void" || schemaText === "undefined") {
462
852
  return;
463
853
  }
464
854
 
@@ -505,9 +895,10 @@ export default {}; // Export an empty object to make it a module
505
895
  const interfaceNameMatches = propText.match(/\b([A-Z][a-zA-Z0-9]*)\s*\[/g);
506
896
  if (interfaceNameMatches) {
507
897
  for (const match of interfaceNameMatches) {
508
- const referencedInterfaceName = match.replace(/\s*\[$/, '').trim();
898
+ const referencedInterfaceName = match.replace(/\s*\[$/, "").trim();
509
899
  // Try to find this interface in the handler file
510
- const referencedInterfaceDecl = handlerFile.getInterface(referencedInterfaceName) || handlerFile.getTypeAlias(referencedInterfaceName);
900
+ const referencedInterfaceDecl =
901
+ handlerFile.getInterface(referencedInterfaceName) || handlerFile.getTypeAlias(referencedInterfaceName);
511
902
  if (referencedInterfaceDecl) {
512
903
  // Interface is in same file - copy it and all its dependencies recursively
513
904
  this.copyInterfaceWithDependencies(referencedInterfaceDecl, handlerFile);
@@ -621,7 +1012,7 @@ export default {}; // Export an empty object to make it a module
621
1012
  const interfaceNameMatches = currentPropTypeText.match(/\b([A-Z][a-zA-Z0-9]*)\s*\[/g);
622
1013
  if (interfaceNameMatches) {
623
1014
  for (const match of interfaceNameMatches) {
624
- const interfaceName = match.replace(/\s*\[$/, '').trim();
1015
+ const interfaceName = match.replace(/\s*\[$/, "").trim();
625
1016
  // Try to find this interface in the handler file
626
1017
  const interfaceDecl = handlerFile.getInterface(interfaceName) || handlerFile.getTypeAlias(interfaceName);
627
1018
  if (interfaceDecl) {
@@ -214,6 +214,11 @@ export function getTypeMetadata(type: Type<ts.Type>) {
214
214
  return [];
215
215
  }
216
216
 
217
+ // Handle empty object literal {} (used in streaming handlers)
218
+ if (type.getText() === "{}") {
219
+ return [];
220
+ }
221
+
217
222
  const symbol = getSymbolOrAlias(type);
218
223
 
219
224
  if (!symbol) {