@flink-app/flink 0.14.3 → 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 (112) hide show
  1. package/CHANGELOG.md +66 -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 +279 -35
  8. package/dist/src/FlinkContext.d.ts +21 -0
  9. package/dist/src/FlinkHttpHandler.d.ts +152 -9
  10. package/dist/src/FlinkHttpHandler.js +37 -1
  11. package/dist/src/TypeScriptCompiler.d.ts +42 -0
  12. package/dist/src/TypeScriptCompiler.js +346 -4
  13. package/dist/src/TypeScriptUtils.js +4 -0
  14. package/dist/src/ai/AgentRunner.d.ts +39 -0
  15. package/dist/src/ai/AgentRunner.js +625 -0
  16. package/dist/src/ai/FlinkAgent.d.ts +446 -0
  17. package/dist/src/ai/FlinkAgent.js +633 -0
  18. package/dist/src/ai/FlinkTool.d.ts +37 -0
  19. package/dist/src/ai/FlinkTool.js +2 -0
  20. package/dist/src/ai/LLMAdapter.d.ts +119 -0
  21. package/dist/src/ai/LLMAdapter.js +2 -0
  22. package/dist/src/ai/SubAgentExecutor.d.ts +36 -0
  23. package/dist/src/ai/SubAgentExecutor.js +220 -0
  24. package/dist/src/ai/ToolExecutor.d.ts +35 -0
  25. package/dist/src/ai/ToolExecutor.js +237 -0
  26. package/dist/src/ai/index.d.ts +5 -0
  27. package/dist/src/ai/index.js +21 -0
  28. package/dist/src/handlers/StreamWriterFactory.d.ts +20 -0
  29. package/dist/src/handlers/StreamWriterFactory.js +83 -0
  30. package/dist/src/index.d.ts +4 -0
  31. package/dist/src/index.js +4 -0
  32. package/dist/src/utils.d.ts +30 -0
  33. package/dist/src/utils.js +52 -0
  34. package/package.json +16 -2
  35. package/readme.md +425 -0
  36. package/spec/AgentDuplicateDetection.spec.ts +112 -0
  37. package/spec/AgentRunner.spec.ts +527 -0
  38. package/spec/ConversationHooks.spec.ts +290 -0
  39. package/spec/FlinkAgent.spec.ts +310 -0
  40. package/spec/FlinkApp.onError.spec.ts +1 -2
  41. package/spec/FlinkApp.query.spec.ts +107 -0
  42. package/spec/FlinkApp.validationMode.spec.ts +155 -0
  43. package/spec/StreamingIntegration.spec.ts +138 -0
  44. package/spec/SubAgentSupport.spec.ts +941 -0
  45. package/spec/ToolExecutor.spec.ts +360 -0
  46. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar.js +57 -0
  47. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar2.js +59 -0
  48. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema.js +53 -0
  49. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema2.js +53 -0
  50. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema3.js +53 -0
  51. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema.js +55 -0
  52. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema2.js +55 -0
  53. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile.js +58 -0
  54. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile2.js +58 -0
  55. package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler.js +53 -0
  56. package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler2.js +55 -0
  57. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchCar.js +58 -0
  58. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOnboardingSession.js +76 -0
  59. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOrderWithComplexTypes.js +58 -0
  60. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchProductWithIntersection.js +59 -0
  61. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchUserWithUnion.js +59 -0
  62. package/spec/mock-project/dist/spec/mock-project/src/handlers/PostCar.js +55 -0
  63. package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogin.js +56 -0
  64. package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogout.js +55 -0
  65. package/spec/mock-project/dist/spec/mock-project/src/handlers/PutCar.js +55 -0
  66. package/spec/mock-project/dist/spec/mock-project/src/index.js +83 -0
  67. package/spec/mock-project/dist/spec/mock-project/src/repos/CarRepo.js +26 -0
  68. package/spec/mock-project/dist/spec/mock-project/src/schemas/Car.js +2 -0
  69. package/spec/mock-project/dist/spec/mock-project/src/schemas/DefaultExportSchema.js +2 -0
  70. package/spec/mock-project/dist/spec/mock-project/src/schemas/FileWithTwoSchemas.js +2 -0
  71. package/spec/mock-project/dist/src/FlinkApp.js +1012 -0
  72. package/spec/mock-project/dist/src/FlinkContext.js +2 -0
  73. package/spec/mock-project/dist/src/FlinkErrors.js +143 -0
  74. package/spec/mock-project/dist/src/FlinkHttpHandler.js +47 -0
  75. package/spec/mock-project/dist/src/FlinkJob.js +2 -0
  76. package/spec/mock-project/dist/src/FlinkLog.js +26 -0
  77. package/spec/mock-project/dist/src/FlinkPlugin.js +2 -0
  78. package/spec/mock-project/dist/src/FlinkRepo.js +224 -0
  79. package/spec/mock-project/dist/src/FlinkResponse.js +2 -0
  80. package/spec/mock-project/dist/src/ai/AgentExecutor.js +279 -0
  81. package/spec/mock-project/dist/src/ai/AgentRunner.js +625 -0
  82. package/spec/mock-project/dist/src/ai/FlinkAgent.js +633 -0
  83. package/spec/mock-project/dist/src/ai/FlinkTool.js +2 -0
  84. package/spec/mock-project/dist/src/ai/LLMAdapter.js +2 -0
  85. package/spec/mock-project/dist/src/ai/SubAgentExecutor.js +220 -0
  86. package/spec/mock-project/dist/src/ai/ToolExecutor.js +237 -0
  87. package/spec/mock-project/dist/src/auth/FlinkAuthPlugin.js +2 -0
  88. package/spec/mock-project/dist/src/auth/FlinkAuthUser.js +2 -0
  89. package/spec/mock-project/dist/src/handlers/StreamWriterFactory.js +83 -0
  90. package/spec/mock-project/dist/src/index.js +17 -69
  91. package/spec/mock-project/dist/src/mock-data-generator.js +9 -0
  92. package/spec/mock-project/dist/src/utils.js +290 -0
  93. package/spec/mock-project/tsconfig.json +6 -1
  94. package/spec/testHelpers.ts +49 -0
  95. package/spec/utils.caseConversion.spec.ts +80 -0
  96. package/spec/utils.spec.ts +13 -13
  97. package/src/FlinkApp.ts +275 -8
  98. package/src/FlinkContext.ts +22 -0
  99. package/src/FlinkHttpHandler.ts +164 -10
  100. package/src/TypeScriptCompiler.ts +398 -7
  101. package/src/TypeScriptUtils.ts +5 -0
  102. package/src/ai/AgentRunner.ts +549 -0
  103. package/src/ai/FlinkAgent.ts +770 -0
  104. package/src/ai/FlinkTool.ts +40 -0
  105. package/src/ai/LLMAdapter.ts +96 -0
  106. package/src/ai/SubAgentExecutor.ts +199 -0
  107. package/src/ai/ToolExecutor.ts +193 -0
  108. package/src/ai/index.ts +5 -0
  109. package/src/handlers/StreamWriterFactory.ts +84 -0
  110. package/src/index.ts +4 -0
  111. package/src/utils.ts +52 -0
  112. package/tsconfig.json +6 -1
@@ -0,0 +1,107 @@
1
+ import { FlinkApp } from "../src/FlinkApp";
2
+ import { FlinkContext } from "../src/FlinkContext";
3
+ import { GetHandler, HttpMethod } from "../src/FlinkHttpHandler";
4
+
5
+ const request = require("supertest");
6
+
7
+ interface TestContext extends FlinkContext {}
8
+
9
+ describe("Query parameter normalization", () => {
10
+ let app: FlinkApp<TestContext>;
11
+
12
+ afterEach(async () => {
13
+ if (app && app.started) {
14
+ await app.stop();
15
+ }
16
+ });
17
+
18
+ it("should normalize query params to strings", async () => {
19
+ const handler: GetHandler<TestContext, any> = async ({ req }) => {
20
+ return { status: 200, data: { query: req.query } };
21
+ };
22
+
23
+ app = new FlinkApp<TestContext>({ name: "test-query-norm", port: 4000 });
24
+ await app.start();
25
+
26
+ app.addHandler({
27
+ default: handler,
28
+ Route: { method: HttpMethod.get, path: "/test" },
29
+ });
30
+
31
+ const response = await request(app.expressApp).get("/test?name=John&age=25");
32
+
33
+ expect(response.status).toBe(200);
34
+ expect(response.body.data.query.name).toBe("John");
35
+ expect(response.body.data.query.age).toBe("25");
36
+ expect(typeof response.body.data.query.name).toBe("string");
37
+ expect(typeof response.body.data.query.age).toBe("string");
38
+ });
39
+
40
+ it("should normalize array query params to string arrays", async () => {
41
+ const handler: GetHandler<TestContext, any> = async ({ req }) => {
42
+ return { status: 200, data: { query: req.query } };
43
+ };
44
+
45
+ app = new FlinkApp<TestContext>({ name: "test-query-array", port: 4001 });
46
+ await app.start();
47
+
48
+ app.addHandler({
49
+ default: handler,
50
+ Route: { method: HttpMethod.get, path: "/test-array" },
51
+ });
52
+
53
+ const response = await request(app.expressApp).get("/test-array?tag=a&tag=b&tag=c");
54
+
55
+ expect(response.status).toBe(200);
56
+ expect(Array.isArray(response.body.data.query.tag)).toBe(true);
57
+ expect(response.body.data.query.tag).toEqual(["a", "b", "c"]);
58
+ response.body.data.query.tag.forEach((tag: string) => {
59
+ expect(typeof tag).toBe("string");
60
+ });
61
+ });
62
+
63
+ it("should normalize numeric values to strings", async () => {
64
+ const handler: GetHandler<TestContext, any> = async ({ req }) => {
65
+ return { status: 200, data: { query: req.query } };
66
+ };
67
+
68
+ app = new FlinkApp<TestContext>({ name: "test-query-numbers", port: 4002 });
69
+ await app.start();
70
+
71
+ app.addHandler({
72
+ default: handler,
73
+ Route: { method: HttpMethod.get, path: "/test-numbers" },
74
+ });
75
+
76
+ const response = await request(app.expressApp).get("/test-numbers?count=100&price=99.99");
77
+
78
+ expect(response.status).toBe(200);
79
+ expect(response.body.data.query.count).toBe("100");
80
+ expect(response.body.data.query.price).toBe("99.99");
81
+ expect(typeof response.body.data.query.count).toBe("string");
82
+ expect(typeof response.body.data.query.price).toBe("string");
83
+ });
84
+
85
+ it("should allow handlers to parse strings as needed", async () => {
86
+ const handler: GetHandler<TestContext, any> = async ({ req }) => {
87
+ const page = Number(req.query.page) || 1;
88
+ const active = req.query.active === "true";
89
+
90
+ return { status: 200, data: { page, active } };
91
+ };
92
+
93
+ app = new FlinkApp<TestContext>({ name: "test-query-parsing", port: 4003 });
94
+ await app.start();
95
+
96
+ app.addHandler({
97
+ default: handler,
98
+ Route: { method: HttpMethod.get, path: "/test-parsing" },
99
+ });
100
+
101
+ const response = await request(app.expressApp).get("/test-parsing?page=2&active=true");
102
+
103
+ expect(response.status).toBe(200);
104
+ expect(response.body.data.page).toBe(2);
105
+ expect(response.body.data.active).toBe(true);
106
+ });
107
+ });
@@ -0,0 +1,155 @@
1
+ import { FlinkApp } from "../src/FlinkApp";
2
+ import { FlinkContext } from "../src/FlinkContext";
3
+ import { Handler, RouteProps, ValidationMode } from "../src/FlinkHttpHandler";
4
+
5
+ interface TestContext extends FlinkContext {}
6
+
7
+ // Test schemas
8
+ interface ValidRequest {
9
+ name: string;
10
+ age: number;
11
+ }
12
+
13
+ interface ValidResponse {
14
+ message: string;
15
+ status: string;
16
+ }
17
+
18
+ describe("FlinkApp validation modes", () => {
19
+ let app: FlinkApp<TestContext>;
20
+
21
+ beforeEach(async () => {
22
+ app = new FlinkApp<TestContext>({
23
+ name: "validation-test-app",
24
+ debug: false,
25
+ });
26
+ });
27
+
28
+ afterEach(async () => {
29
+ if (app && app.started) {
30
+ await app.stop();
31
+ }
32
+ });
33
+
34
+ describe("ValidationMode enum", () => {
35
+ it("should have all validation mode values", () => {
36
+ expect(ValidationMode.Validate).toBe("Validate");
37
+ expect(ValidationMode.SkipValidation).toBe("SkipValidation");
38
+ expect(ValidationMode.ValidateRequest).toBe("ValidateRequest");
39
+ expect(ValidationMode.ValidateResponse).toBe("ValidateResponse");
40
+ });
41
+ });
42
+
43
+ describe("RouteProps validation property", () => {
44
+ it("should accept validation property with ValidationMode.Validate", () => {
45
+ const route: RouteProps = {
46
+ path: "/test",
47
+ validation: ValidationMode.Validate,
48
+ };
49
+
50
+ expect(route.validation).toBe(ValidationMode.Validate);
51
+ });
52
+
53
+ it("should accept validation property with ValidationMode.SkipValidation", () => {
54
+ const route: RouteProps = {
55
+ path: "/test",
56
+ validation: ValidationMode.SkipValidation,
57
+ };
58
+
59
+ expect(route.validation).toBe(ValidationMode.SkipValidation);
60
+ });
61
+
62
+ it("should accept validation property with ValidationMode.ValidateRequest", () => {
63
+ const route: RouteProps = {
64
+ path: "/test",
65
+ validation: ValidationMode.ValidateRequest,
66
+ };
67
+
68
+ expect(route.validation).toBe(ValidationMode.ValidateRequest);
69
+ });
70
+
71
+ it("should accept validation property with ValidationMode.ValidateResponse", () => {
72
+ const route: RouteProps = {
73
+ path: "/test",
74
+ validation: ValidationMode.ValidateResponse,
75
+ };
76
+
77
+ expect(route.validation).toBe(ValidationMode.ValidateResponse);
78
+ });
79
+
80
+ it("should be optional and default to undefined", () => {
81
+ const route: RouteProps = {
82
+ path: "/test",
83
+ };
84
+
85
+ expect(route.validation).toBeUndefined();
86
+ });
87
+ });
88
+
89
+ describe("Handler type compatibility", () => {
90
+ it("should allow handlers with validation modes to compile", () => {
91
+ // This test verifies TypeScript compilation
92
+
93
+ const handler: Handler<TestContext, ValidRequest, ValidResponse> = async ({ ctx, req }) => {
94
+ return {
95
+ status: 200,
96
+ data: {
97
+ message: "success",
98
+ status: "ok",
99
+ },
100
+ };
101
+ };
102
+
103
+ expect(handler).toBeDefined();
104
+ expect(typeof handler).toBe("function");
105
+ });
106
+
107
+ it("should allow route props with different validation modes", () => {
108
+ const routes: RouteProps[] = [
109
+ { path: "/test1", validation: ValidationMode.Validate },
110
+ { path: "/test2", validation: ValidationMode.SkipValidation },
111
+ { path: "/test3", validation: ValidationMode.ValidateRequest },
112
+ { path: "/test4", validation: ValidationMode.ValidateResponse },
113
+ { path: "/test5" }, // No validation specified
114
+ ];
115
+
116
+ expect(routes.length).toBe(5);
117
+ expect(routes[0].validation).toBe(ValidationMode.Validate);
118
+ expect(routes[1].validation).toBe(ValidationMode.SkipValidation);
119
+ expect(routes[2].validation).toBe(ValidationMode.ValidateRequest);
120
+ expect(routes[3].validation).toBe(ValidationMode.ValidateResponse);
121
+ expect(routes[4].validation).toBeUndefined();
122
+ });
123
+ });
124
+
125
+ describe("Backward compatibility", () => {
126
+ it("should not break existing code without validation property", () => {
127
+ const route: RouteProps = {
128
+ path: "/legacy",
129
+ permissions: ["admin"],
130
+ };
131
+
132
+ // TypeScript should compile this without errors
133
+ expect(route.path).toBe("/legacy");
134
+ expect(route.validation).toBeUndefined();
135
+ });
136
+
137
+ it("should allow all existing RouteProps properties with validation", () => {
138
+ const route: RouteProps = {
139
+ path: "/test",
140
+ permissions: ["admin", "user"],
141
+ skipAutoRegister: true,
142
+ docs: "Test endpoint",
143
+ order: -1,
144
+ validation: ValidationMode.SkipValidation,
145
+ };
146
+
147
+ expect(route.path).toBe("/test");
148
+ expect(route.permissions).toEqual(["admin", "user"]);
149
+ expect(route.skipAutoRegister).toBe(true);
150
+ expect(route.docs).toBe("Test endpoint");
151
+ expect(route.order).toBe(-1);
152
+ expect(route.validation).toBe(ValidationMode.SkipValidation);
153
+ });
154
+ });
155
+ });
@@ -0,0 +1,138 @@
1
+ import { FlinkAgent, StreamChunk } from "../src/ai/FlinkAgent";
2
+ import { FlinkContext } from "../src/FlinkContext";
3
+ import { LLMAdapter, LLMStreamChunk } from "../src/ai/LLMAdapter";
4
+
5
+ describe("Streaming Integration", () => {
6
+ let mockCtx: FlinkContext;
7
+
8
+ beforeEach(() => {
9
+ mockCtx = {
10
+ repos: {},
11
+ plugins: {},
12
+ };
13
+ });
14
+
15
+ class StreamingTestAgent extends FlinkAgent<FlinkContext> {
16
+ description = "Test agent for streaming";
17
+ instructions = "You are a test agent";
18
+ tools = [];
19
+
20
+ async query(message: string) {
21
+ const response = this.execute({ message });
22
+ return response;
23
+ }
24
+
25
+ setContext(ctx: FlinkContext) {
26
+ (this as any).ctx = ctx;
27
+ }
28
+
29
+ setMockLLMAdapter(adapter: LLMAdapter) {
30
+ this.__init(new Map([["default", adapter]]), {});
31
+ }
32
+ }
33
+
34
+ it("should stream text deltas in real-time", async () => {
35
+ const agent = new StreamingTestAgent();
36
+ agent.setContext(mockCtx);
37
+
38
+ // Mock LLM that streams chunks
39
+ const mockAdapter: LLMAdapter = {
40
+ stream: async function* () {
41
+ yield { type: "text", delta: "Hello " } as LLMStreamChunk;
42
+ yield { type: "text", delta: "world " } as LLMStreamChunk;
43
+ yield { type: "text", delta: "from agent" } as LLMStreamChunk;
44
+ yield { type: "usage", usage: { inputTokens: 10, outputTokens: 5 } } as LLMStreamChunk;
45
+ yield { type: "done", stopReason: "end_turn" } as LLMStreamChunk;
46
+ },
47
+ };
48
+
49
+ agent.setMockLLMAdapter(mockAdapter);
50
+
51
+ const response = agent.query("Hello");
52
+
53
+ const textChunks: string[] = [];
54
+ for await (const text of (await response).textStream) {
55
+ textChunks.push(text);
56
+ }
57
+
58
+ expect(textChunks.length).toBe(3);
59
+ expect(textChunks.join("")).toBe("Hello world from agent");
60
+ });
61
+
62
+ it("should emit text_delta events during streaming", async () => {
63
+ const agent = new StreamingTestAgent();
64
+ agent.setContext(mockCtx);
65
+
66
+ const mockAdapter: LLMAdapter = {
67
+ stream: async function* () {
68
+ yield { type: "text", delta: "Pro" } as LLMStreamChunk;
69
+ yield { type: "text", delta: "gressive " } as LLMStreamChunk;
70
+ yield { type: "text", delta: "text" } as LLMStreamChunk;
71
+ yield { type: "usage", usage: { inputTokens: 10, outputTokens: 5 } } as LLMStreamChunk;
72
+ yield { type: "done", stopReason: "end_turn" } as LLMStreamChunk;
73
+ },
74
+ };
75
+
76
+ agent.setMockLLMAdapter(mockAdapter);
77
+
78
+ const response = agent.query("Hello");
79
+
80
+ const events: StreamChunk[] = [];
81
+ for await (const chunk of (await response).fullStream) {
82
+ events.push(chunk);
83
+ }
84
+
85
+ const textDeltas = events.filter((e) => e.type === "text_delta");
86
+ expect(textDeltas.length).toBe(3);
87
+ expect((textDeltas[0] as any).delta).toBe("Pro");
88
+ expect((textDeltas[1] as any).delta).toBe("gressive ");
89
+ expect((textDeltas[2] as any).delta).toBe("text");
90
+ });
91
+
92
+ it("should allow consuming result after streaming completes", async () => {
93
+ const agent = new StreamingTestAgent();
94
+ agent.setContext(mockCtx);
95
+
96
+ const mockAdapter: LLMAdapter = {
97
+ stream: async function* () {
98
+ yield { type: "text", delta: "Complete " } as LLMStreamChunk;
99
+ yield { type: "text", delta: "response" } as LLMStreamChunk;
100
+ yield { type: "usage", usage: { inputTokens: 10, outputTokens: 5 } } as LLMStreamChunk;
101
+ yield { type: "done", stopReason: "end_turn" } as LLMStreamChunk;
102
+ },
103
+ };
104
+
105
+ agent.setMockLLMAdapter(mockAdapter);
106
+
107
+ const response = agent.query("Hello");
108
+
109
+ // Consume stream first
110
+ const textChunks: string[] = [];
111
+ for await (const text of (await response).textStream) {
112
+ textChunks.push(text);
113
+ }
114
+
115
+ // Then await result
116
+ const result = await (await response).result;
117
+
118
+ expect(result.message).toContain("Complete response");
119
+ expect(textChunks.join("")).toBe("Complete response");
120
+ });
121
+
122
+ it("should handle streaming errors gracefully", async () => {
123
+ const agent = new StreamingTestAgent();
124
+ agent.setContext(mockCtx);
125
+
126
+ const mockAdapter: LLMAdapter = {
127
+ stream: async function* () {
128
+ throw new Error("Streaming failed");
129
+ },
130
+ };
131
+
132
+ agent.setMockLLMAdapter(mockAdapter);
133
+
134
+ const response = agent.query("Hello");
135
+
136
+ await expectAsync((await response).result).toBeRejectedWithError("Streaming failed");
137
+ });
138
+ });