@flink-app/flink 0.14.3 → 2.0.0-alpha.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1051 -0
- package/SCHEMA_EXTRACTION_ANALYSIS.md +494 -0
- package/SIMPLE_AST_FEASIBILITY.md +570 -0
- package/bin/flink.ts +13 -2
- package/cli/build.ts +24 -44
- package/cli/clean.ts +13 -25
- package/cli/cli-utils.ts +190 -17
- package/cli/dev.ts +252 -0
- package/cli/loadEnvFiles.ts +116 -0
- package/cli/run.ts +45 -62
- package/dist/bin/flink.js +61 -2
- package/dist/cli/build.js +20 -25
- package/dist/cli/clean.js +12 -10
- package/dist/cli/cli-utils.d.ts +34 -3
- package/dist/cli/cli-utils.js +193 -12
- package/dist/cli/dev.d.ts +2 -0
- package/dist/cli/dev.js +279 -0
- package/dist/cli/loadEnvFiles.d.ts +30 -0
- package/dist/cli/loadEnvFiles.js +113 -0
- package/dist/cli/run.js +47 -46
- package/dist/src/DependencyTracker.d.ts +44 -0
- package/dist/src/DependencyTracker.js +239 -0
- package/dist/src/FlinkApp.d.ts +163 -10
- package/dist/src/FlinkApp.js +847 -184
- package/dist/src/FlinkContext.d.ts +41 -0
- package/dist/src/FlinkErrors.d.ts +19 -6
- package/dist/src/FlinkErrors.js +36 -42
- package/dist/src/FlinkHttpHandler.d.ts +219 -26
- package/dist/src/FlinkHttpHandler.js +37 -1
- package/dist/src/FlinkJob.d.ts +10 -0
- package/dist/src/FlinkLog.d.ts +82 -18
- package/dist/src/FlinkLog.js +165 -13
- package/dist/src/FlinkLogFactory.d.ts +288 -0
- package/dist/src/FlinkLogFactory.js +619 -0
- package/dist/src/FlinkRepo.d.ts +10 -2
- package/dist/src/FlinkRepo.js +11 -1
- package/dist/src/FlinkRequestContext.d.ts +63 -0
- package/dist/src/FlinkRequestContext.js +74 -0
- package/dist/src/FlinkResponse.d.ts +6 -0
- package/dist/src/FlinkService.d.ts +38 -0
- package/dist/src/FlinkService.js +46 -0
- package/dist/src/LeaderElection.d.ts +45 -0
- package/dist/src/LeaderElection.js +269 -0
- package/dist/src/SchemaCache.d.ts +84 -0
- package/dist/src/SchemaCache.js +289 -0
- package/dist/src/TypeScriptCompiler.d.ts +161 -51
- package/dist/src/TypeScriptCompiler.js +1253 -617
- package/dist/src/TypeScriptUtils.js +4 -0
- package/dist/src/ai/AgentRunner.d.ts +39 -0
- package/dist/src/ai/AgentRunner.js +760 -0
- package/dist/src/ai/ConversationAgent.d.ts +279 -0
- package/dist/src/ai/ConversationAgent.js +404 -0
- package/dist/src/ai/ConversationFlinkAgent.d.ts +278 -0
- package/dist/src/ai/ConversationFlinkAgent.js +404 -0
- package/dist/src/ai/FlinkAgent.d.ts +690 -0
- package/dist/src/ai/FlinkAgent.js +729 -0
- package/dist/src/ai/FlinkTool.d.ts +135 -0
- package/dist/src/ai/FlinkTool.js +2 -0
- package/dist/src/ai/InMemoryConversationAgent.d.ts +121 -0
- package/dist/src/ai/InMemoryConversationAgent.js +209 -0
- package/dist/src/ai/LLMAdapter.d.ts +148 -0
- package/dist/src/ai/LLMAdapter.js +2 -0
- package/dist/src/ai/PersistentFlinkAgent.d.ts +278 -0
- package/dist/src/ai/PersistentFlinkAgent.js +403 -0
- package/dist/src/ai/SubAgentExecutor.d.ts +38 -0
- package/dist/src/ai/SubAgentExecutor.js +223 -0
- package/dist/src/ai/ToolExecutor.d.ts +64 -0
- package/dist/src/ai/ToolExecutor.js +497 -0
- package/dist/src/ai/agentInstructions.d.ts +68 -0
- package/dist/src/ai/agentInstructions.js +286 -0
- package/dist/src/ai/index.d.ts +8 -0
- package/dist/src/ai/index.js +26 -0
- package/dist/src/ai/instructionFileLoader.d.ts +44 -0
- package/dist/src/ai/instructionFileLoader.js +179 -0
- package/dist/src/auth/FlinkAuthPlugin.d.ts +1 -1
- package/dist/src/handlers/StreamWriterFactory.d.ts +20 -0
- package/dist/src/handlers/StreamWriterFactory.js +83 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.js +17 -0
- package/dist/src/loadPluginSchemas.d.ts +45 -0
- package/dist/src/loadPluginSchemas.js +143 -0
- package/dist/src/schema-extraction/ComplexTypeDetection.d.ts +40 -0
- package/dist/src/schema-extraction/ComplexTypeDetection.js +75 -0
- package/dist/src/schema-extraction/TypeScriptSourceParser.d.ts +321 -0
- package/dist/src/schema-extraction/TypeScriptSourceParser.js +925 -0
- package/dist/src/schema-extraction/TypeScriptSourceParser.spec.d.ts +1 -0
- package/dist/src/schema-extraction/TypeScriptSourceParser.spec.js +233 -0
- package/dist/src/schema-extraction/TypeScriptTokenizer.d.ts +57 -0
- package/dist/src/schema-extraction/TypeScriptTokenizer.js +177 -0
- package/dist/src/schema-extraction/index.d.ts +2 -0
- package/dist/src/schema-extraction/index.js +20 -0
- package/dist/src/schema-extraction/types.d.ts +31 -0
- package/dist/src/schema-extraction/types.js +2 -0
- package/dist/src/utils/loadFlinkConfig.d.ts +53 -0
- package/dist/src/utils/loadFlinkConfig.js +77 -0
- package/dist/src/utils.d.ts +30 -0
- package/dist/src/utils.js +52 -0
- package/dist/src/workers/SchemaGeneratorWorker.d.ts +1 -0
- package/dist/src/workers/SchemaGeneratorWorker.js +49 -0
- package/dist/src/workers/WorkerPool.d.ts +60 -0
- package/dist/src/workers/WorkerPool.js +306 -0
- package/examples/logging-hierarchical-example.ts +125 -0
- package/package.json +29 -4
- package/readme.md +499 -0
- package/spec/AgentDescendantDetection.spec.ts +335 -0
- package/spec/AgentDuplicateDetection.spec.ts +112 -0
- package/spec/AgentObserver.spec.ts +266 -0
- package/spec/AgentRunner.spec.ts +1062 -0
- package/spec/AsyncLocalStorageContext.spec.ts +223 -0
- package/spec/ConversationHooks.spec.ts +257 -0
- package/spec/FlinkAgent.spec.ts +681 -0
- package/spec/FlinkApp.htmlResponse.spec.ts +260 -0
- package/spec/FlinkApp.onError.invocation.spec.ts +151 -0
- package/spec/FlinkApp.onError.spec.ts +1 -2
- package/spec/FlinkApp.query.spec.ts +107 -0
- package/spec/FlinkApp.routeOrdering.spec.ts +61 -0
- package/spec/FlinkApp.undefinedResponse.spec.ts +123 -0
- package/spec/FlinkApp.validationMode.spec.ts +155 -0
- package/spec/FlinkJob.spec.ts +171 -0
- package/spec/FlinkLogFactory.spec.ts +337 -0
- package/spec/FlinkRepo.spec.ts +1 -1
- package/spec/LeaderElection.spec.ts +174 -0
- package/spec/StreamingIntegration.spec.ts +139 -0
- package/spec/ToolExecutor.spec.ts +465 -0
- package/spec/TypeScriptCompiler.spec.ts +1 -1
- package/spec/TypeScriptSourceParser.spec.ts +1215 -0
- package/spec/TypeScriptTokenizer.spec.ts +366 -0
- package/spec/ai/ContextCompaction.spec.ts +405 -0
- package/spec/ai/ConversationAgent.spec.ts +520 -0
- package/spec/ai/InMemoryConversationAgent.spec.ts +144 -0
- package/spec/ai/agentInstructions.spec.ts +358 -0
- package/spec/fixtures/agent-instructions/TestAgent.ts +24 -0
- package/spec/fixtures/agent-instructions/simple.md +3 -0
- package/spec/fixtures/agent-instructions/template.md +18 -0
- package/spec/fixtures/agent-instructions/yaml-format.yaml +9 -0
- package/spec/mock-project/dist/.tsbuildinfo +1 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar.js +56 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar2.js +58 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema.js +52 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema2.js +52 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema3.js +52 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema.js +54 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema2.js +54 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile.js +57 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile2.js +57 -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 +57 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOnboardingSession.js +75 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOrderWithComplexTypes.js +57 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchProductWithIntersection.js +58 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchUserWithUnion.js +58 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PostCar.js +54 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogin.js +55 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogout.js +54 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PutCar.js +54 -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 +1000 -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 +119 -0
- package/spec/mock-project/dist/src/FlinkLogFactory.js +617 -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/FlinkRequestContext.js +74 -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 +632 -0
- package/spec/mock-project/dist/src/ai/ConversationAgent.js +402 -0
- package/spec/mock-project/dist/src/ai/ConversationFlinkAgent.js +422 -0
- package/spec/mock-project/dist/src/ai/FlinkAgent.js +699 -0
- package/spec/mock-project/dist/src/ai/FlinkTool.js +2 -0
- package/spec/mock-project/dist/src/ai/InMemoryConversationAgent.js +209 -0
- package/spec/mock-project/dist/src/ai/LLMAdapter.js +2 -0
- package/spec/mock-project/dist/src/ai/SubAgentExecutor.js +223 -0
- package/spec/mock-project/dist/src/ai/ToolExecutor.js +412 -0
- package/spec/mock-project/dist/src/ai/agentInstructions.js +246 -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/GetCar.js +26 -52
- package/spec/mock-project/dist/src/handlers/GetCar.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCar2.js +32 -54
- package/spec/mock-project/dist/src/handlers/GetCar2.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema.js +26 -48
- package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema2.js +28 -48
- package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema2.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema3.js +29 -48
- package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema3.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema.js +26 -50
- package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema2.js +28 -50
- package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema2.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile.js +27 -53
- package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile2.js +29 -53
- package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile2.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler.js +16 -49
- package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler2.js +25 -50
- package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler2.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PatchCar.js +27 -53
- package/spec/mock-project/dist/src/handlers/PatchCar.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PatchOnboardingSession.js +44 -70
- package/spec/mock-project/dist/src/handlers/PatchOnboardingSession.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PatchOrderWithComplexTypes.js +27 -53
- package/spec/mock-project/dist/src/handlers/PatchOrderWithComplexTypes.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PatchProductWithIntersection.js +28 -54
- package/spec/mock-project/dist/src/handlers/PatchProductWithIntersection.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PatchUserWithUnion.js +28 -54
- package/spec/mock-project/dist/src/handlers/PatchUserWithUnion.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PostCar.js +24 -50
- package/spec/mock-project/dist/src/handlers/PostCar.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PostLogin.js +25 -51
- package/spec/mock-project/dist/src/handlers/PostLogin.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PostLogout.js +24 -50
- package/spec/mock-project/dist/src/handlers/PostLogout.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PutCar.js +24 -50
- package/spec/mock-project/dist/src/handlers/PutCar.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/StreamWriterFactory.js +83 -0
- package/spec/mock-project/dist/src/index.js +52 -76
- package/spec/mock-project/dist/src/index.js.map +1 -0
- package/spec/mock-project/dist/src/mock-data-generator.js +9 -0
- package/spec/mock-project/dist/src/repos/CarRepo.js +12 -24
- package/spec/mock-project/dist/src/repos/CarRepo.js.map +1 -0
- package/spec/mock-project/dist/src/schemas/Car.js +3 -1
- package/spec/mock-project/dist/src/schemas/Car.js.map +1 -0
- package/spec/mock-project/dist/src/schemas/DefaultExportSchema.js +3 -1
- package/spec/mock-project/dist/src/schemas/DefaultExportSchema.js.map +1 -0
- package/spec/mock-project/dist/src/schemas/FileWithTwoSchemas.js +3 -1
- package/spec/mock-project/dist/src/schemas/FileWithTwoSchemas.js.map +1 -0
- package/spec/mock-project/dist/src/utils.js +290 -0
- package/spec/mock-project/tsconfig.json +6 -1
- package/spec/schema-generation-nested-objects.spec.ts +97 -0
- package/spec/testHelpers.ts +49 -0
- package/spec/utils.caseConversion.spec.ts +78 -0
- package/spec/utils.spec.ts +13 -13
- package/src/DependencyTracker.ts +166 -0
- package/src/FlinkApp.ts +919 -155
- package/src/FlinkContext.ts +43 -0
- package/src/FlinkErrors.ts +32 -12
- package/src/FlinkHttpHandler.ts +246 -28
- package/src/FlinkJob.ts +11 -0
- package/src/FlinkLog.ts +119 -12
- package/src/FlinkLogFactory.ts +699 -0
- package/src/FlinkRepo.ts +10 -3
- package/src/FlinkRequestContext.ts +95 -0
- package/src/FlinkResponse.ts +6 -0
- package/src/FlinkService.ts +49 -0
- package/src/LeaderElection.ts +203 -0
- package/src/SchemaCache.ts +232 -0
- package/src/TypeScriptCompiler.ts +1347 -610
- package/src/TypeScriptUtils.ts +5 -0
- package/src/ai/AgentRunner.ts +646 -0
- package/src/ai/ConversationAgent.ts +413 -0
- package/src/ai/FlinkAgent.ts +1069 -0
- package/src/ai/FlinkTool.ts +165 -0
- package/src/ai/InMemoryConversationAgent.ts +149 -0
- package/src/ai/LLMAdapter.ts +126 -0
- package/src/ai/ToolExecutor.ts +485 -0
- package/src/ai/agentInstructions.ts +245 -0
- package/src/ai/index.ts +8 -0
- package/src/ai/instructionFileLoader.ts +156 -0
- package/src/auth/FlinkAuthPlugin.ts +2 -1
- package/src/handlers/StreamWriterFactory.ts +84 -0
- package/src/index.ts +14 -0
- package/src/loadPluginSchemas.ts +141 -0
- package/src/schema-extraction/TypeScriptSourceParser.ts +1058 -0
- package/src/schema-extraction/TypeScriptTokenizer.ts +205 -0
- package/src/schema-extraction/index.ts +2 -0
- package/src/schema-extraction/types.ts +34 -0
- package/src/utils/loadFlinkConfig.ts +89 -0
- package/src/utils.ts +52 -0
- package/tsconfig.json +6 -1
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { FlinkAgent } from "../src/ai/FlinkAgent";
|
|
3
|
+
import { FlinkToolProps } from "../src/ai/FlinkTool";
|
|
4
|
+
import { LLMAdapter } from "../src/ai/LLMAdapter";
|
|
5
|
+
import { ToolExecutor } from "../src/ai/ToolExecutor";
|
|
6
|
+
import { FlinkContext } from "../src/FlinkContext";
|
|
7
|
+
import { createStreamingMock } from "./testHelpers";
|
|
8
|
+
|
|
9
|
+
describe("FlinkAgent", () => {
|
|
10
|
+
let mockCtx: FlinkContext;
|
|
11
|
+
let mockLLMAdapter: LLMAdapter;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
mockCtx = {
|
|
15
|
+
repos: {},
|
|
16
|
+
plugins: {},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Mock LLM Adapter
|
|
20
|
+
mockLLMAdapter = createStreamingMock([
|
|
21
|
+
{
|
|
22
|
+
textContent: "Test response",
|
|
23
|
+
toolCalls: [],
|
|
24
|
+
usage: { inputTokens: 10, outputTokens: 20 },
|
|
25
|
+
stopReason: "end_turn" as const,
|
|
26
|
+
},
|
|
27
|
+
]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("withUser binding", () => {
|
|
31
|
+
class TestAgent extends FlinkAgent<FlinkContext> {
|
|
32
|
+
id = "test-agent";
|
|
33
|
+
description = "Test agent";
|
|
34
|
+
instructions() { return "Test instructions"; }
|
|
35
|
+
tools: string[] = [];
|
|
36
|
+
|
|
37
|
+
async query(message: string) {
|
|
38
|
+
const response = this.execute({ message });
|
|
39
|
+
return await response.result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
setContext(ctx: FlinkContext) {
|
|
43
|
+
(this as any).ctx = ctx;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
it("should bind user to agent instance", () => {
|
|
48
|
+
const agent = new TestAgent();
|
|
49
|
+
agent.setContext(mockCtx);
|
|
50
|
+
|
|
51
|
+
const user = { id: "123", permissions: ["admin"] };
|
|
52
|
+
const boundAgent = agent.withUser(user);
|
|
53
|
+
|
|
54
|
+
// Should return a new instance
|
|
55
|
+
expect(boundAgent).not.toBe(agent);
|
|
56
|
+
// Should have bound user
|
|
57
|
+
expect((boundAgent as any)._boundUser).toBe(user);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should use bound user for agent permission checks", async () => {
|
|
61
|
+
class PermissionedAgent extends TestAgent {
|
|
62
|
+
permissions = "admin";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const agent = new PermissionedAgent();
|
|
66
|
+
agent.setContext(mockCtx);
|
|
67
|
+
agent.__init(new Map([["default", mockLLMAdapter]]), {});
|
|
68
|
+
|
|
69
|
+
const userWithoutPermission = { permissions: [] };
|
|
70
|
+
const userWithPermission = { permissions: ["admin"] };
|
|
71
|
+
|
|
72
|
+
// Should fail without permission
|
|
73
|
+
const boundAgentFail = agent.withUser(userWithoutPermission);
|
|
74
|
+
await expectAsync(boundAgentFail.query("test")).toBeRejected();
|
|
75
|
+
|
|
76
|
+
// Should succeed with permission
|
|
77
|
+
const boundAgentSuccess = agent.withUser(userWithPermission);
|
|
78
|
+
const result = await boundAgentSuccess.query("test");
|
|
79
|
+
expect(result.message).toBeDefined();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should pass bound user to tools", async () => {
|
|
83
|
+
const toolSpy = jasmine.createSpy("toolFn").and.returnValue(Promise.resolve({ success: true, data: { result: "ok" } }));
|
|
84
|
+
|
|
85
|
+
const toolProps: FlinkToolProps = {
|
|
86
|
+
id: "test_tool",
|
|
87
|
+
description: "Test tool",
|
|
88
|
+
inputSchema: z.object({}),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const toolExecutor = new ToolExecutor(toolProps, toolSpy as any, mockCtx);
|
|
92
|
+
const toolsMap = new Map([["test_tool", toolExecutor]]);
|
|
93
|
+
|
|
94
|
+
// Mock LLM to call tool
|
|
95
|
+
const toolMockAdapter = createStreamingMock([
|
|
96
|
+
{
|
|
97
|
+
textContent: undefined,
|
|
98
|
+
toolCalls: [{ id: "1", name: "test_tool", input: {} }],
|
|
99
|
+
usage: { inputTokens: 10, outputTokens: 20 },
|
|
100
|
+
stopReason: "tool_use" as const,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
textContent: "Done",
|
|
104
|
+
toolCalls: [],
|
|
105
|
+
usage: { inputTokens: 10, outputTokens: 20 },
|
|
106
|
+
stopReason: "end_turn" as const,
|
|
107
|
+
},
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
class ToolAgent extends TestAgent {
|
|
111
|
+
tools: string[] = ["test_tool"];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const agent = new ToolAgent();
|
|
115
|
+
agent.setContext(mockCtx);
|
|
116
|
+
agent.__init(new Map([["default", toolMockAdapter]]), Object.fromEntries(toolsMap));
|
|
117
|
+
|
|
118
|
+
const user = { id: "123", permissions: ["admin"] };
|
|
119
|
+
const boundAgent = agent.withUser(user);
|
|
120
|
+
|
|
121
|
+
await boundAgent.query("test");
|
|
122
|
+
|
|
123
|
+
// Tool should have been called with the bound user
|
|
124
|
+
expect(toolSpy).toHaveBeenCalled();
|
|
125
|
+
const toolCall = (toolSpy as jasmine.Spy).calls.mostRecent();
|
|
126
|
+
expect(toolCall.args[0].user).toBe(user);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("Tool permission filtering", () => {
|
|
131
|
+
class TestAgent extends FlinkAgent<FlinkContext> {
|
|
132
|
+
id = "test-agent";
|
|
133
|
+
description = "Test agent";
|
|
134
|
+
instructions() { return "Test instructions"; }
|
|
135
|
+
tools: string[] = ["public_tool", "admin_tool"];
|
|
136
|
+
|
|
137
|
+
async query(message: string) {
|
|
138
|
+
const response = this.execute({ message });
|
|
139
|
+
return await response.result;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
setContext(ctx: FlinkContext) {
|
|
143
|
+
(this as any).ctx = ctx;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
it("should filter out tools user doesn't have permission for", async () => {
|
|
148
|
+
// Create public tool (no permissions)
|
|
149
|
+
const publicToolProps: FlinkToolProps = {
|
|
150
|
+
id: "public_tool",
|
|
151
|
+
description: "Public tool",
|
|
152
|
+
inputSchema: z.object({}),
|
|
153
|
+
};
|
|
154
|
+
const publicToolFn = async () => ({ success: true as const, data: {} });
|
|
155
|
+
const publicTool = new ToolExecutor(publicToolProps, publicToolFn, mockCtx);
|
|
156
|
+
|
|
157
|
+
// Create admin tool (requires admin permission)
|
|
158
|
+
const adminToolProps: FlinkToolProps = {
|
|
159
|
+
id: "admin_tool",
|
|
160
|
+
description: "Admin tool",
|
|
161
|
+
inputSchema: z.object({}),
|
|
162
|
+
permissions: "admin",
|
|
163
|
+
};
|
|
164
|
+
const adminToolFn = async () => ({ success: true as const, data: {} });
|
|
165
|
+
const adminTool = new ToolExecutor(adminToolProps, adminToolFn, mockCtx);
|
|
166
|
+
|
|
167
|
+
const toolsMap = new Map([
|
|
168
|
+
["public_tool", publicTool],
|
|
169
|
+
["admin_tool", adminTool],
|
|
170
|
+
]);
|
|
171
|
+
|
|
172
|
+
const agent = new TestAgent();
|
|
173
|
+
agent.setContext(mockCtx);
|
|
174
|
+
agent.__init(new Map([["default", mockLLMAdapter]]), Object.fromEntries(toolsMap));
|
|
175
|
+
|
|
176
|
+
// User without admin permission
|
|
177
|
+
const regularUser = { permissions: [] };
|
|
178
|
+
const boundAgent = agent.withUser(regularUser);
|
|
179
|
+
|
|
180
|
+
await boundAgent.query("test");
|
|
181
|
+
|
|
182
|
+
// LLM should have been called with only the public tool via stream
|
|
183
|
+
const llmCall = (mockLLMAdapter.stream as jasmine.Spy).calls.mostRecent();
|
|
184
|
+
const toolsProvided = llmCall.args[0].tools;
|
|
185
|
+
|
|
186
|
+
expect(toolsProvided.length).toBe(1);
|
|
187
|
+
expect(toolsProvided[0].name).toBe("public_tool");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("should show all tools to admin users", async () => {
|
|
191
|
+
// Create public and admin tools (same as above)
|
|
192
|
+
const publicToolProps: FlinkToolProps = {
|
|
193
|
+
id: "public_tool",
|
|
194
|
+
description: "Public tool",
|
|
195
|
+
inputSchema: z.object({}),
|
|
196
|
+
};
|
|
197
|
+
const publicToolFn = async () => ({ success: true as const, data: {} });
|
|
198
|
+
const publicTool = new ToolExecutor(publicToolProps, publicToolFn, mockCtx);
|
|
199
|
+
|
|
200
|
+
const adminToolProps: FlinkToolProps = {
|
|
201
|
+
id: "admin_tool",
|
|
202
|
+
description: "Admin tool",
|
|
203
|
+
inputSchema: z.object({}),
|
|
204
|
+
permissions: "admin",
|
|
205
|
+
};
|
|
206
|
+
const adminToolFn = async () => ({ success: true as const, data: {} });
|
|
207
|
+
const adminTool = new ToolExecutor(adminToolProps, adminToolFn, mockCtx);
|
|
208
|
+
|
|
209
|
+
const toolsMap = new Map([
|
|
210
|
+
["public_tool", publicTool],
|
|
211
|
+
["admin_tool", adminTool],
|
|
212
|
+
]);
|
|
213
|
+
|
|
214
|
+
const agent = new TestAgent();
|
|
215
|
+
agent.setContext(mockCtx);
|
|
216
|
+
agent.__init(new Map([["default", mockLLMAdapter]]), Object.fromEntries(toolsMap));
|
|
217
|
+
|
|
218
|
+
// User with admin permission
|
|
219
|
+
const adminUser = { permissions: ["admin"] };
|
|
220
|
+
const boundAgent = agent.withUser(adminUser);
|
|
221
|
+
|
|
222
|
+
await boundAgent.query("test");
|
|
223
|
+
|
|
224
|
+
// LLM should have been called with both tools via stream
|
|
225
|
+
const llmCall = (mockLLMAdapter.stream as jasmine.Spy).calls.mostRecent();
|
|
226
|
+
const toolsProvided = llmCall.args[0].tools;
|
|
227
|
+
|
|
228
|
+
expect(toolsProvided.length).toBe(2);
|
|
229
|
+
expect(toolsProvided.map((t: any) => t.name).sort()).toEqual(["admin_tool", "public_tool"]);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe("Tool references (FlinkToolFile imports)", () => {
|
|
234
|
+
it("should support tool file references in addition to string IDs", () => {
|
|
235
|
+
// Create mock tool file (same structure as real tool files)
|
|
236
|
+
const mockToolFile = {
|
|
237
|
+
Tool: {
|
|
238
|
+
id: "mock-tool",
|
|
239
|
+
description: "Mock tool for testing",
|
|
240
|
+
inputSchema: z.object({ input: z.string() }),
|
|
241
|
+
},
|
|
242
|
+
default: async () => ({ success: true as const, data: "ok" }),
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// Create tool executor from the mock file
|
|
246
|
+
const toolExecutor = new ToolExecutor(mockToolFile.Tool, mockToolFile.default as any, mockCtx);
|
|
247
|
+
|
|
248
|
+
// Set up tools in context (not as Map, but as plain object for _tools)
|
|
249
|
+
// Agent using tool reference instead of string ID
|
|
250
|
+
class TestAgentWithToolRef extends FlinkAgent<FlinkContext> {
|
|
251
|
+
id = "test-agent";
|
|
252
|
+
description = "Test agent";
|
|
253
|
+
instructions() { return "Test instructions"; }
|
|
254
|
+
tools = [mockToolFile]; // Direct tool file reference!
|
|
255
|
+
|
|
256
|
+
async query(message: string) {
|
|
257
|
+
const response = this.execute({ message });
|
|
258
|
+
return await response.result;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const agent = new TestAgentWithToolRef();
|
|
263
|
+
(agent as any).ctx = mockCtx;
|
|
264
|
+
agent.__init(new Map([["default", mockLLMAdapter]]), { "mock-tool": toolExecutor });
|
|
265
|
+
|
|
266
|
+
// Verify resolveTools() correctly extracts tool ID from FlinkToolFile
|
|
267
|
+
const resolvedTools = (agent as any).resolveTools();
|
|
268
|
+
expect(resolvedTools.has("mock-tool")).toBe(true);
|
|
269
|
+
expect(resolvedTools.get("mock-tool")).toBe(toolExecutor);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("should support mixed string IDs and tool file references", () => {
|
|
273
|
+
// Create two tools: one referenced by string, one by file reference
|
|
274
|
+
const mockToolFile = {
|
|
275
|
+
Tool: {
|
|
276
|
+
id: "tool-by-reference",
|
|
277
|
+
description: "Tool referenced by import",
|
|
278
|
+
inputSchema: z.object({}),
|
|
279
|
+
},
|
|
280
|
+
default: async () => ({ success: true as const, data: "ok" }),
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const toolByRefExecutor = new ToolExecutor(mockToolFile.Tool, mockToolFile.default as any, mockCtx);
|
|
284
|
+
const toolByStringExecutor = new ToolExecutor(
|
|
285
|
+
{ id: "tool-by-string", description: "Tool by string", inputSchema: z.object({}) },
|
|
286
|
+
async () => ({ success: true as const, data: "ok" }),
|
|
287
|
+
mockCtx
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
class MixedToolsAgent extends FlinkAgent<FlinkContext> {
|
|
291
|
+
id = "test-agent";
|
|
292
|
+
description = "Test agent";
|
|
293
|
+
instructions() { return "Test instructions"; }
|
|
294
|
+
tools = [
|
|
295
|
+
mockToolFile, // File reference
|
|
296
|
+
"tool-by-string", // String ID
|
|
297
|
+
];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const agent = new MixedToolsAgent();
|
|
301
|
+
(agent as any).ctx = mockCtx;
|
|
302
|
+
agent.__init(new Map([["default", mockLLMAdapter]]), {
|
|
303
|
+
"tool-by-reference": toolByRefExecutor,
|
|
304
|
+
"tool-by-string": toolByStringExecutor,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Both tools should be resolved correctly
|
|
308
|
+
const resolvedTools = (agent as any).resolveTools();
|
|
309
|
+
expect(resolvedTools.size).toBe(2);
|
|
310
|
+
expect(resolvedTools.has("tool-by-reference")).toBe(true);
|
|
311
|
+
expect(resolvedTools.has("tool-by-string")).toBe(true);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
describe("Dynamic Instructions via Callback", () => {
|
|
316
|
+
it("should support static string instructions (backwards compatibility)", async () => {
|
|
317
|
+
class StaticAgent extends FlinkAgent<FlinkContext> {
|
|
318
|
+
id = "test-agent";
|
|
319
|
+
description = "Static agent";
|
|
320
|
+
instructions() { return "Static instructions"; }
|
|
321
|
+
tools: string[] = [];
|
|
322
|
+
|
|
323
|
+
async query(message: string) {
|
|
324
|
+
const response = this.execute({ message });
|
|
325
|
+
return await response.result;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const agent = new StaticAgent();
|
|
330
|
+
(agent as any).ctx = mockCtx;
|
|
331
|
+
agent.__init(new Map([["default", mockLLMAdapter]]), {});
|
|
332
|
+
|
|
333
|
+
const result = await agent.query("Hello");
|
|
334
|
+
|
|
335
|
+
expect(result.message).toBe("Test response");
|
|
336
|
+
expect(mockLLMAdapter.stream).toHaveBeenCalled();
|
|
337
|
+
const callArgs = (mockLLMAdapter.stream as jasmine.Spy).calls.first().args[0];
|
|
338
|
+
expect(callArgs.instructions).toBe("Static instructions");
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("should support synchronous callback", async () => {
|
|
342
|
+
class DynamicAgent extends FlinkAgent<FlinkContext> {
|
|
343
|
+
id = "test-agent";
|
|
344
|
+
description = "Dynamic agent";
|
|
345
|
+
instructions(ctx: FlinkContext, agentContext: any) {
|
|
346
|
+
return `You are a support agent for user ${agentContext.user?.name || "unknown"}`;
|
|
347
|
+
}
|
|
348
|
+
tools: string[] = [];
|
|
349
|
+
|
|
350
|
+
async query(message: string) {
|
|
351
|
+
const response = this.execute({ message });
|
|
352
|
+
return await response.result;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const agent = new DynamicAgent();
|
|
357
|
+
(agent as any).ctx = mockCtx;
|
|
358
|
+
agent.__init(new Map([["default", mockLLMAdapter]]), {});
|
|
359
|
+
|
|
360
|
+
const boundAgent = agent.withUser({ id: "123", name: "John Doe" });
|
|
361
|
+
const result = await boundAgent.query("Hello");
|
|
362
|
+
|
|
363
|
+
expect(result.message).toBe("Test response");
|
|
364
|
+
expect(mockLLMAdapter.stream).toHaveBeenCalled();
|
|
365
|
+
const callArgs = (mockLLMAdapter.stream as jasmine.Spy).calls.first().args[0];
|
|
366
|
+
expect(callArgs.instructions).toBe("You are a support agent for user John Doe");
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("should support async callback with database lookup", async () => {
|
|
370
|
+
const mockUser = { id: "123", name: "Jane Smith", tier: "premium" };
|
|
371
|
+
const mockRepoCtx = {
|
|
372
|
+
repos: {
|
|
373
|
+
userRepo: {
|
|
374
|
+
getById: jasmine.createSpy("getById").and.returnValue(Promise.resolve(mockUser)),
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
plugins: {},
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
class DatabaseAgent extends FlinkAgent<any> {
|
|
381
|
+
id = "test-agent";
|
|
382
|
+
description = "Database agent";
|
|
383
|
+
async instructions(ctx: any, agentContext: any) {
|
|
384
|
+
if (!agentContext.user) {
|
|
385
|
+
return "You are a support agent.";
|
|
386
|
+
}
|
|
387
|
+
const profile = await ctx.repos.userRepo.getById(agentContext.user.id);
|
|
388
|
+
return `You are a support agent. Customer: ${profile.name}, Tier: ${profile.tier}`;
|
|
389
|
+
}
|
|
390
|
+
tools: string[] = [];
|
|
391
|
+
|
|
392
|
+
async query(message: string) {
|
|
393
|
+
const response = this.execute({ message });
|
|
394
|
+
return await response.result;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const agent = new DatabaseAgent();
|
|
399
|
+
(agent as any).ctx = mockRepoCtx;
|
|
400
|
+
agent.__init(new Map([["default", mockLLMAdapter]]), {});
|
|
401
|
+
|
|
402
|
+
const boundAgent = agent.withUser({ id: "123" });
|
|
403
|
+
const result = await boundAgent.query("Hello");
|
|
404
|
+
|
|
405
|
+
expect(mockRepoCtx.repos.userRepo.getById).toHaveBeenCalledWith("123");
|
|
406
|
+
expect(result.message).toBe("Test response");
|
|
407
|
+
expect(mockLLMAdapter.stream).toHaveBeenCalled();
|
|
408
|
+
const callArgs = (mockLLMAdapter.stream as jasmine.Spy).calls.first().args[0];
|
|
409
|
+
expect(callArgs.instructions).toBe("You are a support agent. Customer: Jane Smith, Tier: premium");
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("should work with runner caching", async () => {
|
|
413
|
+
let callCount = 0;
|
|
414
|
+
|
|
415
|
+
class CachedAgent extends FlinkAgent<FlinkContext> {
|
|
416
|
+
id = "test-agent";
|
|
417
|
+
description = "Cached agent";
|
|
418
|
+
instructions(ctx: FlinkContext, agentContext: any) {
|
|
419
|
+
callCount++;
|
|
420
|
+
return `Instructions for user ${agentContext.user?.name || "unknown"}`;
|
|
421
|
+
}
|
|
422
|
+
tools: string[] = [];
|
|
423
|
+
|
|
424
|
+
async query(message: string) {
|
|
425
|
+
const response = this.execute({ message });
|
|
426
|
+
return await response.result;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const agent = new CachedAgent();
|
|
431
|
+
(agent as any).ctx = mockCtx;
|
|
432
|
+
agent.__init(new Map([["default", mockLLMAdapter]]), {});
|
|
433
|
+
|
|
434
|
+
// First execution with user1
|
|
435
|
+
const user1 = { id: "1", name: "Alice" };
|
|
436
|
+
const boundAgent1 = agent.withUser(user1);
|
|
437
|
+
await boundAgent1.query("Hello");
|
|
438
|
+
|
|
439
|
+
expect(callCount).toBe(1);
|
|
440
|
+
expect(mockLLMAdapter.stream).toHaveBeenCalled();
|
|
441
|
+
let callArgs = (mockLLMAdapter.stream as jasmine.Spy).calls.first().args[0];
|
|
442
|
+
expect(callArgs.instructions).toBe("Instructions for user Alice");
|
|
443
|
+
|
|
444
|
+
// Reset mock
|
|
445
|
+
(mockLLMAdapter.stream as jasmine.Spy).calls.reset();
|
|
446
|
+
|
|
447
|
+
// Second execution with different user (new bound agent = new runner)
|
|
448
|
+
const user2 = { id: "2", name: "Bob" };
|
|
449
|
+
const boundAgent2 = agent.withUser(user2);
|
|
450
|
+
await boundAgent2.query("Hello");
|
|
451
|
+
|
|
452
|
+
expect(callCount).toBe(2); // Should increment because new runner instance
|
|
453
|
+
expect(mockLLMAdapter.stream).toHaveBeenCalled();
|
|
454
|
+
callArgs = (mockLLMAdapter.stream as jasmine.Spy).calls.first().args[0];
|
|
455
|
+
expect(callArgs.instructions).toBe("Instructions for user Bob");
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it("should handle callback with conversationId", async () => {
|
|
459
|
+
let capturedContext: any;
|
|
460
|
+
|
|
461
|
+
class ConversationAgent extends FlinkAgent<FlinkContext> {
|
|
462
|
+
id = "test-agent";
|
|
463
|
+
description = "Conversation agent";
|
|
464
|
+
instructions(ctx: FlinkContext, agentContext: any) {
|
|
465
|
+
capturedContext = agentContext;
|
|
466
|
+
return `Conversation: ${agentContext.conversationId || "new"}`;
|
|
467
|
+
}
|
|
468
|
+
tools: string[] = [];
|
|
469
|
+
|
|
470
|
+
async query(message: string, conversationId?: string) {
|
|
471
|
+
const response = this.execute({ message, conversationId });
|
|
472
|
+
return await response.result;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const agent = new ConversationAgent();
|
|
477
|
+
(agent as any).ctx = mockCtx;
|
|
478
|
+
agent.__init(new Map([["default", mockLLMAdapter]]), {});
|
|
479
|
+
|
|
480
|
+
await agent.query("Hello", "conv-456");
|
|
481
|
+
|
|
482
|
+
expect(capturedContext.conversationId).toBe("conv-456");
|
|
483
|
+
expect(mockLLMAdapter.stream).toHaveBeenCalled();
|
|
484
|
+
const callArgs = (mockLLMAdapter.stream as jasmine.Spy).calls.first().args[0];
|
|
485
|
+
expect(callArgs.instructions).toBe("Conversation: conv-456");
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it("should throw error if callback throws", async () => {
|
|
489
|
+
class ErrorAgent extends FlinkAgent<FlinkContext> {
|
|
490
|
+
id = "test-agent";
|
|
491
|
+
description = "Error agent";
|
|
492
|
+
instructions(): string {
|
|
493
|
+
throw new Error("Database unavailable");
|
|
494
|
+
}
|
|
495
|
+
tools: string[] = [];
|
|
496
|
+
|
|
497
|
+
async query(message: string) {
|
|
498
|
+
const response = this.execute({ message });
|
|
499
|
+
return await response.result;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const agent = new ErrorAgent();
|
|
504
|
+
(agent as any).ctx = mockCtx;
|
|
505
|
+
agent.__init(new Map([["default", mockLLMAdapter]]), {});
|
|
506
|
+
|
|
507
|
+
await expectAsync(agent.query("Hello")).toBeRejectedWithError(/Failed to resolve instructions/);
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
describe("withConversationContext binding", () => {
|
|
512
|
+
interface TestConversationContext {
|
|
513
|
+
sessionId: string;
|
|
514
|
+
featureFlags: Record<string, boolean>;
|
|
515
|
+
requestId?: string;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
class TestAgent extends FlinkAgent<FlinkContext, TestConversationContext> {
|
|
519
|
+
id = "test-agent";
|
|
520
|
+
description = "Test agent";
|
|
521
|
+
instructions() { return "Test instructions"; }
|
|
522
|
+
tools: string[] = [];
|
|
523
|
+
|
|
524
|
+
async query(message: string) {
|
|
525
|
+
const response = this.execute({ message });
|
|
526
|
+
return await response.result;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
setContext(ctx: FlinkContext) {
|
|
530
|
+
(this as any).ctx = ctx;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
it("should bind conversation context to agent instance", () => {
|
|
535
|
+
const agent = new TestAgent();
|
|
536
|
+
agent.setContext(mockCtx);
|
|
537
|
+
|
|
538
|
+
const conversationCtx: TestConversationContext = {
|
|
539
|
+
sessionId: "session-123",
|
|
540
|
+
featureFlags: { enableNewUI: true },
|
|
541
|
+
requestId: "req-456",
|
|
542
|
+
};
|
|
543
|
+
const boundAgent = agent.withConversationContext(conversationCtx);
|
|
544
|
+
|
|
545
|
+
// Should return a new instance
|
|
546
|
+
expect(boundAgent).not.toBe(agent);
|
|
547
|
+
// Should have bound conversation context
|
|
548
|
+
expect((boundAgent as any)._boundConversationContext).toBe(conversationCtx);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it("should pass bound conversation context to tools", async () => {
|
|
552
|
+
const toolSpy = jasmine.createSpy("toolFn").and.returnValue(Promise.resolve({ success: true, data: { result: "ok" } }));
|
|
553
|
+
|
|
554
|
+
const toolProps: FlinkToolProps = {
|
|
555
|
+
id: "test_tool",
|
|
556
|
+
description: "Test tool",
|
|
557
|
+
inputSchema: z.object({}),
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
const toolExecutor = new ToolExecutor(toolProps, toolSpy as any, mockCtx);
|
|
561
|
+
const toolsMap = new Map([["test_tool", toolExecutor]]);
|
|
562
|
+
|
|
563
|
+
// Mock LLM to call tool
|
|
564
|
+
const toolMockAdapter = createStreamingMock([
|
|
565
|
+
{
|
|
566
|
+
textContent: undefined,
|
|
567
|
+
toolCalls: [{ id: "1", name: "test_tool", input: {} }],
|
|
568
|
+
usage: { inputTokens: 10, outputTokens: 20 },
|
|
569
|
+
stopReason: "tool_use" as const,
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
textContent: "Done",
|
|
573
|
+
toolCalls: [],
|
|
574
|
+
usage: { inputTokens: 10, outputTokens: 20 },
|
|
575
|
+
stopReason: "end_turn" as const,
|
|
576
|
+
},
|
|
577
|
+
]);
|
|
578
|
+
|
|
579
|
+
class ToolAgent extends TestAgent {
|
|
580
|
+
tools = ["test_tool"];
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const agent = new ToolAgent();
|
|
584
|
+
agent.setContext(mockCtx);
|
|
585
|
+
agent.__init(new Map([["default", toolMockAdapter]]), Object.fromEntries(toolsMap));
|
|
586
|
+
|
|
587
|
+
const conversationCtx: TestConversationContext = {
|
|
588
|
+
sessionId: "session-123",
|
|
589
|
+
featureFlags: { enableNewUI: true },
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
const boundAgent = agent.withConversationContext(conversationCtx);
|
|
593
|
+
await boundAgent.query("test");
|
|
594
|
+
|
|
595
|
+
// Verify tool was called with conversation context
|
|
596
|
+
expect(toolSpy).toHaveBeenCalled();
|
|
597
|
+
const toolCallArgs = toolSpy.calls.first().args[0];
|
|
598
|
+
expect(toolCallArgs.conversationCtx).toBe(conversationCtx);
|
|
599
|
+
expect(toolCallArgs.conversationCtx.sessionId).toBe("session-123");
|
|
600
|
+
expect(toolCallArgs.conversationCtx.featureFlags.enableNewUI).toBe(true);
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
it("should support fluent chaining with other binding methods", () => {
|
|
604
|
+
const agent = new TestAgent();
|
|
605
|
+
agent.setContext(mockCtx);
|
|
606
|
+
agent.__init(new Map([["default", mockLLMAdapter]]), {});
|
|
607
|
+
|
|
608
|
+
const user = { id: "123", permissions: ["admin"] };
|
|
609
|
+
const conversationCtx: TestConversationContext = {
|
|
610
|
+
sessionId: "session-123",
|
|
611
|
+
featureFlags: { enableNewUI: true },
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
// Chain withUser and withConversationContext
|
|
615
|
+
const boundAgent = agent.withUser(user).withConversationContext(conversationCtx);
|
|
616
|
+
|
|
617
|
+
expect((boundAgent as any)._boundUser).toBe(user);
|
|
618
|
+
expect((boundAgent as any)._boundConversationContext).toBe(conversationCtx);
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it("should allow explicit conversationContext in execute to override bound", async () => {
|
|
622
|
+
const toolSpy = jasmine.createSpy("toolFn").and.returnValue(Promise.resolve({ success: true, data: { result: "ok" } }));
|
|
623
|
+
|
|
624
|
+
const toolProps: FlinkToolProps = {
|
|
625
|
+
id: "test_tool",
|
|
626
|
+
description: "Test tool",
|
|
627
|
+
inputSchema: z.object({}),
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
const toolExecutor = new ToolExecutor(toolProps, toolSpy as any, mockCtx);
|
|
631
|
+
const toolsMap = new Map([["test_tool", toolExecutor]]);
|
|
632
|
+
|
|
633
|
+
const toolMockAdapter = createStreamingMock([
|
|
634
|
+
{
|
|
635
|
+
textContent: undefined,
|
|
636
|
+
toolCalls: [{ id: "1", name: "test_tool", input: {} }],
|
|
637
|
+
usage: { inputTokens: 10, outputTokens: 20 },
|
|
638
|
+
stopReason: "tool_use" as const,
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
textContent: "Done",
|
|
642
|
+
toolCalls: [],
|
|
643
|
+
usage: { inputTokens: 10, outputTokens: 20 },
|
|
644
|
+
stopReason: "end_turn" as const,
|
|
645
|
+
},
|
|
646
|
+
]);
|
|
647
|
+
|
|
648
|
+
class ToolAgent extends TestAgent {
|
|
649
|
+
tools = ["test_tool"];
|
|
650
|
+
|
|
651
|
+
async queryWithContext(message: string, ctx: TestConversationContext) {
|
|
652
|
+
const response = this.execute({ message, conversationContext: ctx });
|
|
653
|
+
return await response.result;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const agent = new ToolAgent();
|
|
658
|
+
agent.setContext(mockCtx);
|
|
659
|
+
agent.__init(new Map([["default", toolMockAdapter]]), Object.fromEntries(toolsMap));
|
|
660
|
+
|
|
661
|
+
const boundCtx: TestConversationContext = {
|
|
662
|
+
sessionId: "bound-session",
|
|
663
|
+
featureFlags: {},
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
const explicitCtx: TestConversationContext = {
|
|
667
|
+
sessionId: "explicit-session",
|
|
668
|
+
featureFlags: { newFeature: true },
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
const boundAgent = agent.withConversationContext(boundCtx);
|
|
672
|
+
await boundAgent.queryWithContext("test", explicitCtx);
|
|
673
|
+
|
|
674
|
+
// Verify tool was called with explicit context (override)
|
|
675
|
+
expect(toolSpy).toHaveBeenCalled();
|
|
676
|
+
const toolCallArgs = toolSpy.calls.first().args[0];
|
|
677
|
+
expect(toolCallArgs.conversationCtx.sessionId).toBe("explicit-session");
|
|
678
|
+
expect(toolCallArgs.conversationCtx.featureFlags.newFeature).toBe(true);
|
|
679
|
+
});
|
|
680
|
+
});
|
|
681
|
+
});
|