@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
package/src/FlinkApp.ts CHANGED
@@ -9,6 +9,10 @@ import morgan from "morgan";
9
9
  import ms from "ms";
10
10
  import { AsyncTask, CronJob, SimpleIntervalJob, ToadScheduler } from "toad-scheduler";
11
11
  import { v4 } from "uuid";
12
+ import { FlinkAgentFile } from "./ai/FlinkAgent";
13
+ import { FlinkToolFile } from "./ai/FlinkTool";
14
+ import { LLMAdapter } from "./ai/LLMAdapter";
15
+ import { ToolExecutor } from "./ai/ToolExecutor";
12
16
  import { FlinkAuthPlugin } from "./auth/FlinkAuthPlugin";
13
17
  import { FlinkContext } from "./FlinkContext";
14
18
  import { FlinkError, internalServerError, notFound, unauthorized } from "./FlinkErrors";
@@ -18,6 +22,7 @@ import { log } from "./FlinkLog";
18
22
  import { FlinkPlugin } from "./FlinkPlugin";
19
23
  import { FlinkRepo } from "./FlinkRepo";
20
24
  import { FlinkResponse } from "./FlinkResponse";
25
+ import { StreamWriterFactory } from "./handlers/StreamWriterFactory";
21
26
  import generateMockData from "./mock-data-generator";
22
27
  import { formatValidationErrors, getPathParams, isError } from "./utils";
23
28
 
@@ -63,6 +68,18 @@ export const autoRegisteredRepos: {
63
68
  */
64
69
  export const autoRegisteredJobs: FlinkJobFile[] = [];
65
70
 
71
+ /**
72
+ * This will be populated at compile time when the apps tools
73
+ * are picked up by TypeScript compiler
74
+ */
75
+ export const autoRegisteredTools: FlinkToolFile[] = [];
76
+
77
+ /**
78
+ * This will be populated at compile time when the apps agents
79
+ * are picked up by TypeScript compiler
80
+ */
81
+ export const autoRegisteredAgents: FlinkAgentFile[] = [];
82
+
66
83
  export interface FlinkOptions {
67
84
  /**
68
85
  * Name of application, will only show in logs and in HTTP header.
@@ -175,6 +192,15 @@ export interface FlinkOptions {
175
192
  // autoAssignCollection?: string;
176
193
  };
177
194
 
195
+ /**
196
+ * AI configuration for agents and tools
197
+ * Register LLM adapters with custom IDs (e.g., "anthropic", "openai", "anthropic-eu", etc.)
198
+ * This allows multiple adapters of the same type with different configurations
199
+ */
200
+ ai?: {
201
+ llms?: { [id: string]: LLMAdapter };
202
+ };
203
+
178
204
  /**
179
205
  * If true, the HTTP server will be disabled.
180
206
  * Only useful when starting a Flink app for testing purposes.
@@ -294,6 +320,10 @@ export class FlinkApp<C extends FlinkContext> {
294
320
 
295
321
  private repos: { [x: string]: FlinkRepo<C, any> } = {};
296
322
 
323
+ private llmAdapters: Map<string, LLMAdapter> = new Map();
324
+ private tools: { [x: string]: ToolExecutor<C> } = {};
325
+ private agents: { [x: string]: any } = {}; // FlinkAgent<C> instances
326
+
297
327
  /**
298
328
  * Internal cache used to track registered handlers and potentially any overlapping routes
299
329
  */
@@ -314,14 +344,20 @@ export class FlinkApp<C extends FlinkContext> {
314
344
  this.rawContentTypes = Array.isArray(opts.rawContentTypes)
315
345
  ? opts.rawContentTypes
316
346
  : typeof opts.rawContentTypes === "string"
317
- ? [opts.rawContentTypes]
318
- : undefined;
347
+ ? [opts.rawContentTypes]
348
+ : undefined;
319
349
  this.auth = opts.auth;
320
350
  this.jsonOptions = opts.jsonOptions || { limit: "1mb" };
321
351
  this.schedulingOptions = opts.scheduling;
322
352
  this.disableHttpServer = !!opts.disableHttpServer;
323
353
  this.accessLog = { enabled: true, format: "dev", ...opts.accessLog };
324
354
  this.onError = opts.onError;
355
+
356
+ // Register LLM adapters if configured
357
+ if (opts.ai?.llms) {
358
+ // Convert plain object to Map for internal use
359
+ this.llmAdapters = new Map(Object.entries(opts.ai.llms));
360
+ }
325
361
  }
326
362
 
327
363
  get ctx() {
@@ -342,6 +378,7 @@ export class FlinkApp<C extends FlinkContext> {
342
378
  log.bgColorLog("cyan", `Init db took ${offsetTime - startTime} ms`);
343
379
  }
344
380
 
381
+ // Build initial context (without agents - they'll be added later)
345
382
  await this.buildContext();
346
383
 
347
384
  if (this.debug) {
@@ -349,6 +386,30 @@ export class FlinkApp<C extends FlinkContext> {
349
386
  offsetTime = Date.now();
350
387
  }
351
388
 
389
+ // Register tools (needs context for ToolExecutor)
390
+ await this.registerAutoRegisterableTools();
391
+
392
+ if (this.debug) {
393
+ log.bgColorLog("cyan", `Register tools took ${Date.now() - offsetTime} ms`);
394
+ offsetTime = Date.now();
395
+ }
396
+
397
+ // Register agents (creates agent instances)
398
+ await this.registerAutoRegisterableAgents();
399
+
400
+ if (this.debug) {
401
+ log.bgColorLog("cyan", `Register agents took ${Date.now() - offsetTime} ms`);
402
+ offsetTime = Date.now();
403
+ }
404
+
405
+ // Initialize agents now that context and tools are ready
406
+ await this.initializeAgents();
407
+
408
+ if (this.debug) {
409
+ log.bgColorLog("cyan", `Initialize agents took ${Date.now() - offsetTime} ms`);
410
+ offsetTime = Date.now();
411
+ }
412
+
352
413
  if (this.isSchedulingEnabled) {
353
414
  this.scheduler = new ToadScheduler();
354
415
  } else {
@@ -519,7 +580,7 @@ export class FlinkApp<C extends FlinkContext> {
519
580
  this.handlers.push(handlerConfig);
520
581
 
521
582
  const { routeProps, schema = {} } = handlerConfig;
522
- const { method } = routeProps;
583
+ const { method, streamFormat } = routeProps;
523
584
 
524
585
  if (!method) {
525
586
  log.error(`Route ${routeProps.path} is missing http method`);
@@ -543,8 +604,8 @@ export class FlinkApp<C extends FlinkContext> {
543
604
  validateReq = ajv.compile(schema.reqSchema);
544
605
  }
545
606
 
546
- // Compile response schema if validation mode requires it
547
- if (schema.resSchema && validationMode !== ValidationMode.SkipValidation && validationMode !== ValidationMode.ValidateRequest) {
607
+ // Skip response validation for streaming handlers (responses are stream chunks, not final JSON)
608
+ if (!streamFormat && schema.resSchema && validationMode !== ValidationMode.SkipValidation && validationMode !== ValidationMode.ValidateRequest) {
548
609
  validateRes = ajv.compile(schema.resSchema);
549
610
  }
550
611
 
@@ -573,7 +634,8 @@ export class FlinkApp<C extends FlinkContext> {
573
634
  }
574
635
  }
575
636
 
576
- if (routeProps.mockApi && schema.resSchema) {
637
+ // Skip mock API for streaming handlers
638
+ if (routeProps.mockApi && schema.resSchema && !streamFormat) {
577
639
  log.warn(`Mock response for ${req.method.toUpperCase()} ${req.path}`);
578
640
 
579
641
  const data = generateMockData(schema.resSchema);
@@ -603,6 +665,9 @@ export class FlinkApp<C extends FlinkContext> {
603
665
  req.query = normalizedQuery;
604
666
  }
605
667
 
668
+ // Create stream writer if streaming handler
669
+ const stream = streamFormat ? StreamWriterFactory.create(res, streamFormat) : undefined;
670
+
606
671
  let handlerRes: FlinkResponse<any>;
607
672
 
608
673
  try {
@@ -611,8 +676,19 @@ export class FlinkApp<C extends FlinkContext> {
611
676
  req: req as FlinkRequest,
612
677
  ctx: this.ctx,
613
678
  origin: routeProps.origin,
679
+ stream,
614
680
  });
615
681
  } catch (err: any) {
682
+ // Handle errors for streaming handlers
683
+ if (streamFormat && stream) {
684
+ log.error(`Streaming handler error on ${req.method.toUpperCase()} ${req.path}: ${err.message}`, {
685
+ error: err,
686
+ path: req.path,
687
+ method: req.method,
688
+ });
689
+ stream.error(err);
690
+ return;
691
+ }
616
692
  let errorResponse: FlinkResponse<FlinkError>;
617
693
 
618
694
  // duck typing to check if it is a FlinkError
@@ -656,6 +732,16 @@ export class FlinkApp<C extends FlinkContext> {
656
732
  return res.status(errorResponse.status || 500).json(errorResponse);
657
733
  }
658
734
 
735
+ // Skip response handling for streaming handlers (stream controls response lifecycle)
736
+ if (streamFormat) {
737
+ return;
738
+ }
739
+
740
+ // Ensure handlerRes is defined for non-streaming handlers
741
+ if (!handlerRes) {
742
+ return res.status(204).send();
743
+ }
744
+
659
745
  if (validateRes && !isError(handlerRes)) {
660
746
  const valid = validateRes(JSON.parse(JSON.stringify(handlerRes.data)));
661
747
 
@@ -684,7 +770,7 @@ export class FlinkApp<C extends FlinkContext> {
684
770
  return process.exit(1); // TODO: Do we need to exit?
685
771
  } else {
686
772
  this.handlerRouteCache.set(methodAndRoute, JSON.stringify(routeProps));
687
- log.info(`Registered route ${methodAndRoute}`);
773
+ log.info(`Registered ${streamFormat ? 'streaming ' : ''}route ${methodAndRoute}${streamFormat ? ` (${streamFormat})` : ''}`);
688
774
  }
689
775
  }
690
776
  }
@@ -836,6 +922,151 @@ export class FlinkApp<C extends FlinkContext> {
836
922
  // repoInstance.ctx = this.ctx;
837
923
  }
838
924
 
925
+ private async registerAutoRegisterableTools() {
926
+ const { ToolExecutor } = require("./ai/ToolExecutor");
927
+ const { getRepoInstanceName } = require("./utils");
928
+
929
+ for (const toolFile of autoRegisteredTools) {
930
+ if (!toolFile.Tool) {
931
+ log.error(`Missing FlinkToolProps export in tool ${toolFile.__file}`);
932
+ continue;
933
+ }
934
+
935
+ if (!toolFile.default) {
936
+ log.error(`Missing exported tool function in tool ${toolFile.__file}`);
937
+ continue;
938
+ }
939
+
940
+ const toolId = toolFile.Tool.id;
941
+ if (!toolId) {
942
+ log.error(`Tool ${toolFile.__file} missing 'id' property`);
943
+ continue;
944
+ }
945
+
946
+ const toolInstanceName = getRepoInstanceName(toolId);
947
+
948
+ const toolExecutor = new ToolExecutor(toolFile.Tool, toolFile.default, this.ctx);
949
+ this.tools[toolInstanceName] = toolExecutor;
950
+
951
+ log.info(`Registered tool ${toolInstanceName} (${toolId})`);
952
+ }
953
+ }
954
+
955
+ private async registerAutoRegisterableAgents() {
956
+ const { getRepoInstanceName, toKebabCase } = require("./utils");
957
+ const { SubAgentExecutor } = require("./ai/SubAgentExecutor");
958
+
959
+ for (const agentFile of autoRegisteredAgents) {
960
+ // agentFile now exports a class, not a config object
961
+ const AgentClass = agentFile.default;
962
+
963
+ if (!AgentClass) {
964
+ log.error(`Missing default export in agent ${agentFile.__file}`);
965
+ continue;
966
+ }
967
+
968
+ // Instantiate agent (similar to repo instantiation)
969
+ const agentInstance = new AgentClass();
970
+
971
+ // Derive instance name from class name (camelCase)
972
+ const agentInstanceName = getRepoInstanceName(AgentClass.name);
973
+
974
+ // Get agent ID (kebab-case) - either explicit or derived
975
+ const agentId = agentInstance.id || toKebabCase(AgentClass.name);
976
+
977
+ // Check for duplicate instance name
978
+ if (this.agents[agentInstanceName]) {
979
+ const existingAgent = this.agents[agentInstanceName];
980
+ throw new Error(
981
+ `Duplicate agent instance name: "${agentInstanceName}". ` +
982
+ `Agent class "${AgentClass.name}" conflicts with existing agent "${existingAgent.constructor.name}". ` +
983
+ `Instance names are derived by lowercasing the first letter of the class name. ` +
984
+ `Rename one of the classes or use a unique explicit 'id' property.`
985
+ );
986
+ }
987
+
988
+ // Check for duplicate agent ID
989
+ const existingAgentWithSameId = Object.values(this.agents).find(
990
+ (agent: any) => {
991
+ const existingId = agent.id || toKebabCase(agent.constructor.name);
992
+ return existingId === agentId;
993
+ }
994
+ );
995
+
996
+ if (existingAgentWithSameId) {
997
+ throw new Error(
998
+ `Duplicate agent ID: "${agentId}". ` +
999
+ `Agent class "${AgentClass.name}" conflicts with existing agent "${existingAgentWithSameId.constructor.name}". ` +
1000
+ `Agent IDs are derived from class names using kebab-case (e.g., CarAgent → car-agent). ` +
1001
+ `Use an explicit 'id' property to resolve this conflict:\n` +
1002
+ ` id = "my-unique-id";`
1003
+ );
1004
+ }
1005
+
1006
+ // Validate tools exist
1007
+ for (const toolRef of agentInstance.tools) {
1008
+ // Handle both string IDs and tool file references
1009
+ const toolId = typeof toolRef === "string"
1010
+ ? toolRef
1011
+ : toolRef.Tool.id; // Extract ID from FlinkToolFile
1012
+
1013
+ const tool = this.tools[toolId];
1014
+
1015
+ if (!tool) {
1016
+ log.error(`Agent ${AgentClass.name} references tool ${toolId} which is not registered`);
1017
+ throw new Error(`Invalid tool reference in agent ${AgentClass.name}`);
1018
+ }
1019
+ }
1020
+
1021
+ // Validate and register sub-agents
1022
+ if (agentInstance.agents && agentInstance.agents.length > 0) {
1023
+ for (const agentRef of agentInstance.agents) {
1024
+ // Get instance name directly from class reference or string
1025
+ const subAgentInstanceName = typeof agentRef === "string" ? agentRef : getRepoInstanceName(agentRef.name);
1026
+
1027
+ // Validate that sub-agent will exist (will be registered in this loop)
1028
+ // For now, just log - actual validation happens at runtime
1029
+ log.debug(`Agent ${AgentClass.name} references sub-agent ${subAgentInstanceName}`);
1030
+
1031
+ // Create a SubAgentExecutor as a special tool
1032
+ const subAgentToolName = `ask_${subAgentInstanceName}`;
1033
+ const subAgentExecutor = new SubAgentExecutor(subAgentInstanceName, this.ctx);
1034
+
1035
+ // Register as a tool so it appears in the agent's tool list
1036
+ this.tools[subAgentToolName] = subAgentExecutor as any;
1037
+ log.debug(`Created sub-agent tool ${subAgentToolName} for ${subAgentInstanceName}`);
1038
+ }
1039
+ }
1040
+
1041
+ // Register agent (duplicate checks already performed above)
1042
+ this.agents[agentInstanceName] = agentInstance;
1043
+ log.info(`Registered agent ${agentInstanceName} (${AgentClass.name}) with ID: ${agentId}`);
1044
+ }
1045
+
1046
+ // Second pass: validate all sub-agent references
1047
+ for (const agentFile of autoRegisteredAgents) {
1048
+ const AgentClass = agentFile.default;
1049
+
1050
+ if (!AgentClass) {
1051
+ continue;
1052
+ }
1053
+
1054
+ const agentInstance = new AgentClass();
1055
+
1056
+ if (agentInstance.agents && agentInstance.agents.length > 0) {
1057
+ for (const agentRef of agentInstance.agents) {
1058
+ // Get instance name directly from class reference or string
1059
+ const subAgentInstanceName = typeof agentRef === "string" ? agentRef : getRepoInstanceName(agentRef.name);
1060
+
1061
+ if (!this.agents[subAgentInstanceName]) {
1062
+ log.error(`Agent ${AgentClass.name} references sub-agent ${subAgentInstanceName} which is not registered`);
1063
+ throw new Error(`Invalid sub-agent reference in agent ${AgentClass.name}: ${subAgentInstanceName}`);
1064
+ }
1065
+ }
1066
+ }
1067
+ }
1068
+ }
1069
+
839
1070
  /**
840
1071
  * Constructs the app context. Will inject context in all components
841
1072
  * except for handlers which are handled in later stage.
@@ -865,6 +1096,7 @@ export class FlinkApp<C extends FlinkContext> {
865
1096
  repos: this.repos,
866
1097
  plugins: pluginCtx,
867
1098
  auth: this.auth,
1099
+ agents: this.agents,
868
1100
  } as C;
869
1101
 
870
1102
  for (const repo of Object.values(this.repos)) {
@@ -872,6 +1104,18 @@ export class FlinkApp<C extends FlinkContext> {
872
1104
  }
873
1105
  }
874
1106
 
1107
+ /**
1108
+ * Initialize agents after they've been registered and context is ready
1109
+ * Must be called after registerAutoRegisterableAgents()
1110
+ */
1111
+ private async initializeAgents() {
1112
+ // Inject context and initialize agents
1113
+ for (const agent of Object.values(this.agents)) {
1114
+ agent.ctx = this.ctx;
1115
+ agent.__init(this.llmAdapters, this.tools);
1116
+ }
1117
+ }
1118
+
875
1119
  /**
876
1120
  * Connects to database.
877
1121
  */
@@ -1,5 +1,6 @@
1
1
  import { FlinkAuthPlugin } from "./auth/FlinkAuthPlugin";
2
2
  import { FlinkRepo } from "./FlinkRepo";
3
+ import { FlinkAgent } from "./ai/FlinkAgent";
3
4
 
4
5
  export interface FlinkContext<P = any> {
5
6
  repos: {
@@ -12,4 +13,25 @@ export interface FlinkContext<P = any> {
12
13
  * Type of authentication, if any.
13
14
  */
14
15
  auth?: FlinkAuthPlugin;
16
+
17
+ /**
18
+ * AI namespace containing agents
19
+ *
20
+ * Define agents directly in your context interface:
21
+ *
22
+ * @example
23
+ * interface AppCtx extends FlinkContext<PluginCtx> {
24
+ * auth: JwtAuthPlugin;
25
+ * repos: {
26
+ * carRepo: CarRepo;
27
+ * };
28
+ * agents: {
29
+ * carAgent: CarAgent;
30
+ * userAgent: UserAgent;
31
+ * };
32
+ * }
33
+ */
34
+ agents?: {
35
+ [x: string]: FlinkAgent<any>;
36
+ };
15
37
  }
@@ -62,9 +62,54 @@ type Params = Record<string, string>;
62
62
  type Query = Record<string, string | string[]>;
63
63
 
64
64
  /**
65
- * Flink request extends express Request but adds reqId and user object.
65
+ * Stream format for streaming handlers.
66
+ * - sse: Server-Sent Events (text/event-stream)
67
+ * - ndjson: Newline-Delimited JSON (application/x-ndjson)
66
68
  */
67
- export type FlinkRequest<T = any, P = Params, Q = Query> = Request<P, any, T, Q> & { reqId: string; user?: any };
69
+ export type StreamFormat = "sse" | "ndjson";
70
+
71
+ /**
72
+ * Stream writer interface for SSE/NDJSON streaming.
73
+ *
74
+ * Provides methods to write data chunks, handle errors, and manage the stream lifecycle.
75
+ * Only available in handlers where streamFormat is specified in RouteProps.
76
+ */
77
+ export interface StreamWriter<T = any> {
78
+ /**
79
+ * Write data to the stream.
80
+ * Data is automatically JSON-stringified and formatted according to streamFormat.
81
+ */
82
+ write(data: T): void;
83
+
84
+ /**
85
+ * Send error to client and close the stream.
86
+ */
87
+ error(error: Error | string): void;
88
+
89
+ /**
90
+ * Close the stream gracefully.
91
+ */
92
+ end(): void;
93
+
94
+ /**
95
+ * Check if stream is still open (client connected).
96
+ * Returns false if client has disconnected.
97
+ */
98
+ isOpen(): boolean;
99
+ }
100
+
101
+ /**
102
+ * Flink request extends express Request but adds reqId, user object, and userPermissions.
103
+ *
104
+ * userPermissions is populated by auth plugins during authentication and contains
105
+ * the resolved permissions array based on the plugin's configuration (roles, dynamic
106
+ * roles, custom permissions, etc.)
107
+ */
108
+ export type FlinkRequest<T = any, P = Params, Q = Query> = Request<P, any, T, Q> & {
109
+ reqId: string;
110
+ user?: any;
111
+ userPermissions?: string[]; // Resolved permissions from auth plugin
112
+ };
68
113
 
69
114
  /**
70
115
  * Route props to control routing.
@@ -99,6 +144,27 @@ export interface RouteProps {
99
144
 
100
145
  /**
101
146
  * Set permissions needed to access route if route requires authentication.
147
+ *
148
+ * When an array is provided, the user must have **ALL** permissions in the array (AND logic).
149
+ * To require any one of multiple permissions (OR logic), implement custom permission
150
+ * checking in the handler using the `user.permissions` array.
151
+ *
152
+ * @example
153
+ * ```typescript
154
+ * // Single permission
155
+ * permissions: "car:create"
156
+ *
157
+ * // Multiple permissions (user must have ALL)
158
+ * permissions: ["car:create", "car:premium"]
159
+ *
160
+ * // OR logic requires custom implementation
161
+ * const handler = async ({ ctx, user }) => {
162
+ * if (!user.permissions.includes("car:admin") && !user.permissions.includes("car:moderator")) {
163
+ * throw forbidden("Need admin or moderator permission");
164
+ * }
165
+ * // ... handler logic
166
+ * };
167
+ * ```
102
168
  */
103
169
  permissions?: string | string[];
104
170
 
@@ -147,16 +213,48 @@ export interface RouteProps {
147
213
  * @default ValidationMode.Validate
148
214
  */
149
215
  validation?: ValidationMode;
216
+
217
+ /**
218
+ * Stream format for streaming handlers (SSE or NDJSON).
219
+ *
220
+ * When specified, the handler becomes a streaming handler and receives a `stream`
221
+ * parameter for writing data chunks. Response validation is automatically skipped
222
+ * for streaming handlers (chunks are progressive, not a final JSON response).
223
+ *
224
+ * **Formats:**
225
+ * - sse: Server-Sent Events (text/event-stream) - ideal for browser EventSource API
226
+ * - ndjson: Newline-Delimited JSON (application/x-ndjson) - ideal for LLM text streaming
227
+ *
228
+ * **Example:**
229
+ * ```typescript
230
+ * export const Route: RouteProps = {
231
+ * path: "/ai/stream",
232
+ * streamFormat: "sse"
233
+ * };
234
+ *
235
+ * const handler: GetHandler<{}, void> = async ({ ctx, stream }) => {
236
+ * if (!stream) throw new Error("Stream not available");
237
+ * stream.write({ message: "Hello" });
238
+ * stream.end();
239
+ * };
240
+ * ```
241
+ */
242
+ streamFormat?: StreamFormat;
150
243
  }
151
244
 
152
245
  /**
153
246
  * Http handler function that handlers implements in order to
154
247
  * handle HTTP requests and return a JSON response.
248
+ *
249
+ * For streaming handlers (when streamFormat is specified in RouteProps),
250
+ * the stream parameter is available. Streaming handlers should still return
251
+ * a FlinkResponse (can be empty), but it will be ignored by the framework.
155
252
  */
156
253
  export type Handler<Ctx extends FlinkContext, ReqSchema = any, ResSchema = any, P extends Params = Params, Q extends Query = Query> = (props: {
157
254
  req: FlinkRequest<ReqSchema, P, Q>;
158
255
  ctx: Ctx;
159
256
  origin?: string;
257
+ stream?: StreamWriter;
160
258
  }) => Promise<FlinkResponse<ResSchema | FlinkError>>;
161
259
 
162
260
  /**