@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.
- package/CHANGELOG.md +6 -0
- package/cli/build.ts +8 -1
- package/cli/run.ts +8 -1
- package/dist/cli/build.js +8 -1
- package/dist/cli/run.js +8 -1
- package/dist/src/FlinkApp.d.ts +33 -0
- package/dist/src/FlinkApp.js +247 -27
- package/dist/src/FlinkContext.d.ts +21 -0
- package/dist/src/FlinkHttpHandler.d.ts +90 -1
- package/dist/src/TypeScriptCompiler.d.ts +42 -0
- package/dist/src/TypeScriptCompiler.js +346 -4
- package/dist/src/TypeScriptUtils.js +4 -0
- package/dist/src/ai/AgentRunner.d.ts +39 -0
- package/dist/src/ai/AgentRunner.js +625 -0
- package/dist/src/ai/FlinkAgent.d.ts +446 -0
- package/dist/src/ai/FlinkAgent.js +633 -0
- package/dist/src/ai/FlinkTool.d.ts +37 -0
- package/dist/src/ai/FlinkTool.js +2 -0
- package/dist/src/ai/LLMAdapter.d.ts +119 -0
- package/dist/src/ai/LLMAdapter.js +2 -0
- package/dist/src/ai/SubAgentExecutor.d.ts +36 -0
- package/dist/src/ai/SubAgentExecutor.js +220 -0
- package/dist/src/ai/ToolExecutor.d.ts +35 -0
- package/dist/src/ai/ToolExecutor.js +237 -0
- package/dist/src/ai/index.d.ts +5 -0
- package/dist/src/ai/index.js +21 -0
- package/dist/src/handlers/StreamWriterFactory.d.ts +20 -0
- package/dist/src/handlers/StreamWriterFactory.js +83 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +4 -0
- package/dist/src/utils.d.ts +30 -0
- package/dist/src/utils.js +52 -0
- package/package.json +14 -2
- package/readme.md +425 -0
- package/spec/AgentDuplicateDetection.spec.ts +112 -0
- package/spec/AgentRunner.spec.ts +527 -0
- package/spec/ConversationHooks.spec.ts +290 -0
- package/spec/FlinkAgent.spec.ts +310 -0
- package/spec/FlinkApp.onError.spec.ts +1 -2
- package/spec/StreamingIntegration.spec.ts +138 -0
- package/spec/SubAgentSupport.spec.ts +941 -0
- package/spec/ToolExecutor.spec.ts +360 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar.js +57 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar2.js +59 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema.js +53 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema2.js +53 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema3.js +53 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema.js +55 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema2.js +55 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile.js +58 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile2.js +58 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler.js +53 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler2.js +55 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchCar.js +58 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOnboardingSession.js +76 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOrderWithComplexTypes.js +58 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchProductWithIntersection.js +59 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchUserWithUnion.js +59 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PostCar.js +55 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogin.js +56 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogout.js +55 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PutCar.js +55 -0
- package/spec/mock-project/dist/spec/mock-project/src/index.js +83 -0
- package/spec/mock-project/dist/spec/mock-project/src/repos/CarRepo.js +26 -0
- package/spec/mock-project/dist/spec/mock-project/src/schemas/Car.js +2 -0
- package/spec/mock-project/dist/spec/mock-project/src/schemas/DefaultExportSchema.js +2 -0
- package/spec/mock-project/dist/spec/mock-project/src/schemas/FileWithTwoSchemas.js +2 -0
- package/spec/mock-project/dist/src/FlinkApp.js +1012 -0
- package/spec/mock-project/dist/src/FlinkContext.js +2 -0
- package/spec/mock-project/dist/src/FlinkErrors.js +143 -0
- package/spec/mock-project/dist/src/FlinkHttpHandler.js +47 -0
- package/spec/mock-project/dist/src/FlinkJob.js +2 -0
- package/spec/mock-project/dist/src/FlinkLog.js +26 -0
- package/spec/mock-project/dist/src/FlinkPlugin.js +2 -0
- package/spec/mock-project/dist/src/FlinkRepo.js +224 -0
- package/spec/mock-project/dist/src/FlinkResponse.js +2 -0
- package/spec/mock-project/dist/src/ai/AgentExecutor.js +279 -0
- package/spec/mock-project/dist/src/ai/AgentRunner.js +625 -0
- package/spec/mock-project/dist/src/ai/FlinkAgent.js +633 -0
- package/spec/mock-project/dist/src/ai/FlinkTool.js +2 -0
- package/spec/mock-project/dist/src/ai/LLMAdapter.js +2 -0
- package/spec/mock-project/dist/src/ai/SubAgentExecutor.js +220 -0
- package/spec/mock-project/dist/src/ai/ToolExecutor.js +237 -0
- package/spec/mock-project/dist/src/auth/FlinkAuthPlugin.js +2 -0
- package/spec/mock-project/dist/src/auth/FlinkAuthUser.js +2 -0
- package/spec/mock-project/dist/src/handlers/StreamWriterFactory.js +83 -0
- package/spec/mock-project/dist/src/index.js +17 -69
- package/spec/mock-project/dist/src/mock-data-generator.js +9 -0
- package/spec/mock-project/dist/src/utils.js +290 -0
- package/spec/mock-project/tsconfig.json +6 -1
- package/spec/testHelpers.ts +49 -0
- package/spec/utils.caseConversion.spec.ts +80 -0
- package/spec/utils.spec.ts +13 -13
- package/src/FlinkApp.ts +251 -7
- package/src/FlinkContext.ts +22 -0
- package/src/FlinkHttpHandler.ts +100 -2
- package/src/TypeScriptCompiler.ts +398 -7
- package/src/TypeScriptUtils.ts +5 -0
- package/src/ai/AgentRunner.ts +549 -0
- package/src/ai/FlinkAgent.ts +770 -0
- package/src/ai/FlinkTool.ts +40 -0
- package/src/ai/LLMAdapter.ts +96 -0
- package/src/ai/SubAgentExecutor.ts +199 -0
- package/src/ai/ToolExecutor.ts +193 -0
- package/src/ai/index.ts +5 -0
- package/src/handlers/StreamWriterFactory.ts +84 -0
- package/src/index.ts +4 -0
- package/src/utils.ts +52 -0
- 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
|
-
|
|
318
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
*/
|
package/src/FlinkContext.ts
CHANGED
|
@@ -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
|
}
|
package/src/FlinkHttpHandler.ts
CHANGED
|
@@ -62,9 +62,54 @@ type Params = Record<string, string>;
|
|
|
62
62
|
type Query = Record<string, string | string[]>;
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
|
-
*
|
|
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
|
|
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
|
/**
|