@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,1062 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { AgentRunner } from "../src/ai/AgentRunner";
|
|
3
|
+
import { FlinkAgentProps } from "../src/ai/FlinkAgent";
|
|
4
|
+
import { ToolExecutor } from "../src/ai/ToolExecutor";
|
|
5
|
+
import { FlinkContext } from "../src/FlinkContext";
|
|
6
|
+
import { FlinkToolProps } from "../src/ai/FlinkTool";
|
|
7
|
+
import { LLMAdapter } from "../src/ai/LLMAdapter";
|
|
8
|
+
import { createStreamingMock } from "./testHelpers";
|
|
9
|
+
|
|
10
|
+
describe("AgentRunner", () => {
|
|
11
|
+
let mockCtx: FlinkContext;
|
|
12
|
+
let mockLLMAdapter: LLMAdapter;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
mockCtx = {
|
|
16
|
+
repos: {},
|
|
17
|
+
plugins: {},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Mock LLM Adapter with default response
|
|
21
|
+
mockLLMAdapter = createStreamingMock([
|
|
22
|
+
{
|
|
23
|
+
textContent: "Test response",
|
|
24
|
+
toolCalls: [],
|
|
25
|
+
usage: { inputTokens: 10, outputTokens: 20 },
|
|
26
|
+
stopReason: "end_turn" as const,
|
|
27
|
+
},
|
|
28
|
+
]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("Basic execution", () => {
|
|
32
|
+
it("should execute simple agent without tools", async () => {
|
|
33
|
+
const agentProps: FlinkAgentProps<typeof mockCtx> = {
|
|
34
|
+
id: "test_agent",
|
|
35
|
+
description: "Test agent",
|
|
36
|
+
instructions: "You are a helpful assistant",
|
|
37
|
+
tools: [],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const toolsMap = new Map();
|
|
41
|
+
const llmAdapters = new Map();
|
|
42
|
+
llmAdapters.set("default", mockLLMAdapter);
|
|
43
|
+
const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
|
|
44
|
+
|
|
45
|
+
const generator = runner.streamGenerator({ message: "Hello" });
|
|
46
|
+
const chunks: any[] = [];
|
|
47
|
+
|
|
48
|
+
for await (const chunk of generator) {
|
|
49
|
+
chunks.push(chunk);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Should have complete event
|
|
53
|
+
expect(chunks.length).toBeGreaterThan(0);
|
|
54
|
+
const completeChunk = chunks.find((c) => c.type === "complete");
|
|
55
|
+
expect(completeChunk).toBeDefined();
|
|
56
|
+
expect(completeChunk.result.message).toBe("Test response");
|
|
57
|
+
expect(completeChunk.result.stepsUsed).toBeGreaterThan(0);
|
|
58
|
+
expect(completeChunk.result.toolCalls).toEqual([]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should track token usage", async () => {
|
|
62
|
+
const agentProps: FlinkAgentProps<typeof mockCtx> = {
|
|
63
|
+
id: "test_agent",
|
|
64
|
+
description: "Test agent",
|
|
65
|
+
instructions: "You are a helpful assistant",
|
|
66
|
+
tools: [],
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const toolsMap = new Map();
|
|
70
|
+
const llmAdapters = new Map();
|
|
71
|
+
llmAdapters.set("default", mockLLMAdapter);
|
|
72
|
+
const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
|
|
73
|
+
|
|
74
|
+
const generator = runner.streamGenerator({ message: "Hello" });
|
|
75
|
+
|
|
76
|
+
for await (const chunk of generator) {
|
|
77
|
+
if (chunk.type === "complete") {
|
|
78
|
+
expect(chunk.result.usage).toBeDefined();
|
|
79
|
+
expect(chunk.result.usage?.inputTokens).toBeGreaterThan(0);
|
|
80
|
+
expect(chunk.result.usage?.outputTokens).toBeGreaterThan(0);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("Tool calling", () => {
|
|
87
|
+
it("should execute tool calls", async () => {
|
|
88
|
+
// Create mock tool
|
|
89
|
+
const toolProps: FlinkToolProps = {
|
|
90
|
+
id: "get_weather",
|
|
91
|
+
description: "Get weather",
|
|
92
|
+
inputSchema: z.object({ city: z.string() }),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const toolFn = jasmine.createSpy("toolFn").and.returnValue(
|
|
96
|
+
Promise.resolve({
|
|
97
|
+
success: true,
|
|
98
|
+
data: { temperature: 22, conditions: "sunny" },
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const toolExecutor = new ToolExecutor(toolProps, toolFn as any, mockCtx);
|
|
103
|
+
const toolsMap = new Map([["get_weather", toolExecutor]]);
|
|
104
|
+
|
|
105
|
+
// Mock LLM adapter response with tool call
|
|
106
|
+
const weatherMockAdapter = createStreamingMock([
|
|
107
|
+
// First call: agent requests tool
|
|
108
|
+
{
|
|
109
|
+
textContent: "Let me check the weather",
|
|
110
|
+
toolCalls: [
|
|
111
|
+
{
|
|
112
|
+
id: "tool_1",
|
|
113
|
+
name: "get_weather",
|
|
114
|
+
input: { city: "Stockholm" },
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
usage: { inputTokens: 10, outputTokens: 20 },
|
|
118
|
+
stopReason: "tool_use" as const,
|
|
119
|
+
},
|
|
120
|
+
// Second call: agent responds with result
|
|
121
|
+
{
|
|
122
|
+
textContent: "It's sunny and 22°C",
|
|
123
|
+
toolCalls: [],
|
|
124
|
+
usage: { inputTokens: 15, outputTokens: 10 },
|
|
125
|
+
stopReason: "end_turn" as const,
|
|
126
|
+
},
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
const agentProps: FlinkAgentProps<typeof mockCtx> = {
|
|
130
|
+
id: "weather_agent",
|
|
131
|
+
description: "Weather assistant",
|
|
132
|
+
instructions: "You help with weather",
|
|
133
|
+
tools: ["get_weather"],
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const llmAdapters = new Map();
|
|
137
|
+
llmAdapters.set("default", weatherMockAdapter);
|
|
138
|
+
const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
|
|
139
|
+
|
|
140
|
+
const generator = runner.streamGenerator({ message: "What's the weather in Stockholm?" });
|
|
141
|
+
|
|
142
|
+
for await (const chunk of generator) {
|
|
143
|
+
if (chunk.type === "complete") {
|
|
144
|
+
expect(chunk.result.message).toBe("It's sunny and 22°C");
|
|
145
|
+
expect(chunk.result.toolCalls.length).toBe(1);
|
|
146
|
+
expect(chunk.result.toolCalls[0].name).toBe("get_weather");
|
|
147
|
+
expect(chunk.result.toolCalls[0].input).toEqual({ city: "Stockholm" });
|
|
148
|
+
expect(chunk.result.toolCalls[0].output).toEqual({
|
|
149
|
+
temperature: 22,
|
|
150
|
+
conditions: "sunny",
|
|
151
|
+
});
|
|
152
|
+
expect(chunk.result.stepsUsed).toBe(2);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Verify tool was called
|
|
157
|
+
expect(toolFn).toHaveBeenCalledWith({
|
|
158
|
+
input: { city: "Stockholm" },
|
|
159
|
+
ctx: mockCtx,
|
|
160
|
+
user: undefined,
|
|
161
|
+
permissions: undefined,
|
|
162
|
+
conversationCtx: undefined,
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("should handle tool errors gracefully", async () => {
|
|
167
|
+
// Create mock tool that returns error
|
|
168
|
+
const toolProps: FlinkToolProps = {
|
|
169
|
+
id: "get_weather",
|
|
170
|
+
description: "Get weather",
|
|
171
|
+
inputSchema: z.object({ city: z.string() }),
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const toolFn = jasmine.createSpy("toolFn").and.returnValue(
|
|
175
|
+
Promise.resolve({
|
|
176
|
+
success: false,
|
|
177
|
+
error: "API unavailable",
|
|
178
|
+
code: "SERVICE_ERROR",
|
|
179
|
+
})
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const toolExecutor = new ToolExecutor(toolProps, toolFn as any, mockCtx);
|
|
183
|
+
const toolsMap = new Map([["get_weather", toolExecutor]]);
|
|
184
|
+
|
|
185
|
+
// Mock LLM adapter response with tool call
|
|
186
|
+
const errorMockAdapter = createStreamingMock([
|
|
187
|
+
// First call: agent requests tool
|
|
188
|
+
{
|
|
189
|
+
textContent: undefined,
|
|
190
|
+
toolCalls: [
|
|
191
|
+
{
|
|
192
|
+
id: "tool_1",
|
|
193
|
+
name: "get_weather",
|
|
194
|
+
input: { city: "Stockholm" },
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
usage: { inputTokens: 10, outputTokens: 20 },
|
|
198
|
+
stopReason: "tool_use" as const,
|
|
199
|
+
},
|
|
200
|
+
// Second call: agent handles error
|
|
201
|
+
{
|
|
202
|
+
textContent: "Sorry, weather service is unavailable",
|
|
203
|
+
toolCalls: [],
|
|
204
|
+
usage: { inputTokens: 15, outputTokens: 10 },
|
|
205
|
+
stopReason: "end_turn" as const,
|
|
206
|
+
},
|
|
207
|
+
]);
|
|
208
|
+
|
|
209
|
+
const agentProps: FlinkAgentProps<typeof mockCtx> = {
|
|
210
|
+
id: "weather_agent",
|
|
211
|
+
description: "Weather assistant",
|
|
212
|
+
instructions: "You help with weather",
|
|
213
|
+
tools: ["get_weather"],
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const llmAdapters = new Map();
|
|
217
|
+
llmAdapters.set("default", errorMockAdapter);
|
|
218
|
+
const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
|
|
219
|
+
|
|
220
|
+
const generator = runner.streamGenerator({ message: "What's the weather?" });
|
|
221
|
+
|
|
222
|
+
for await (const chunk of generator) {
|
|
223
|
+
if (chunk.type === "complete") {
|
|
224
|
+
expect(chunk.result.toolCalls.length).toBe(1);
|
|
225
|
+
expect(chunk.result.toolCalls[0].error).toBe("API unavailable");
|
|
226
|
+
expect(chunk.result.toolCalls[0].output).toBeNull();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("should handle missing tools", async () => {
|
|
232
|
+
const toolsMap = new Map(); // No tools registered
|
|
233
|
+
|
|
234
|
+
// Mock LLM adapter response requesting non-existent tool
|
|
235
|
+
const missingToolMockAdapter = createStreamingMock([
|
|
236
|
+
{
|
|
237
|
+
textContent: undefined,
|
|
238
|
+
toolCalls: [
|
|
239
|
+
{
|
|
240
|
+
id: "tool_1",
|
|
241
|
+
name: "missing_tool",
|
|
242
|
+
input: {},
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
usage: { inputTokens: 10, outputTokens: 20 },
|
|
246
|
+
stopReason: "tool_use" as const,
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
textContent: "Tool not available",
|
|
250
|
+
toolCalls: [],
|
|
251
|
+
usage: { inputTokens: 15, outputTokens: 10 },
|
|
252
|
+
stopReason: "end_turn" as const,
|
|
253
|
+
},
|
|
254
|
+
]);
|
|
255
|
+
|
|
256
|
+
const agentProps: FlinkAgentProps<typeof mockCtx> = {
|
|
257
|
+
id: "test_agent",
|
|
258
|
+
description: "Test agent",
|
|
259
|
+
instructions: "You are helpful",
|
|
260
|
+
tools: [],
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const llmAdapters = new Map();
|
|
264
|
+
llmAdapters.set("default", missingToolMockAdapter);
|
|
265
|
+
const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
|
|
266
|
+
|
|
267
|
+
const generator = runner.streamGenerator({ message: "Test" });
|
|
268
|
+
|
|
269
|
+
for await (const chunk of generator) {
|
|
270
|
+
if (chunk.type === "complete") {
|
|
271
|
+
expect(chunk.result.toolCalls.length).toBe(1);
|
|
272
|
+
expect(chunk.result.toolCalls[0].error).toContain("not found");
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe("Step limits", () => {
|
|
279
|
+
it("should respect maxSteps limit", async () => {
|
|
280
|
+
// Mock infinite tool calling loop
|
|
281
|
+
const infiniteLoopMockAdapter = createStreamingMock([
|
|
282
|
+
{
|
|
283
|
+
textContent: undefined,
|
|
284
|
+
toolCalls: [
|
|
285
|
+
{
|
|
286
|
+
id: "tool_1",
|
|
287
|
+
name: "test_tool",
|
|
288
|
+
input: {},
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
usage: { inputTokens: 10, outputTokens: 20 },
|
|
292
|
+
stopReason: "tool_use" as const,
|
|
293
|
+
},
|
|
294
|
+
]);
|
|
295
|
+
|
|
296
|
+
const toolProps: FlinkToolProps = {
|
|
297
|
+
id: "test_tool",
|
|
298
|
+
description: "Test tool",
|
|
299
|
+
inputSchema: z.object({}),
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const toolFn = async () => ({ success: true as const, data: {} });
|
|
303
|
+
const toolExecutor = new ToolExecutor(toolProps, toolFn, mockCtx);
|
|
304
|
+
const toolsMap = new Map([["test_tool", toolExecutor]]);
|
|
305
|
+
|
|
306
|
+
const agentProps: FlinkAgentProps<typeof mockCtx> = {
|
|
307
|
+
id: "test_agent",
|
|
308
|
+
description: "Test agent",
|
|
309
|
+
instructions: "You are helpful",
|
|
310
|
+
tools: ["test_tool"],
|
|
311
|
+
limits: { maxSteps: 3 },
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const llmAdapters = new Map();
|
|
315
|
+
llmAdapters.set("default", infiniteLoopMockAdapter);
|
|
316
|
+
const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
|
|
317
|
+
|
|
318
|
+
const generator = runner.streamGenerator({ message: "Test" });
|
|
319
|
+
|
|
320
|
+
for await (const chunk of generator) {
|
|
321
|
+
if (chunk.type === "complete") {
|
|
322
|
+
expect(chunk.result.stepsUsed).toBe(3);
|
|
323
|
+
expect(chunk.result.stoppedEarly).toBe(true);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("should allow runtime maxSteps override", async () => {
|
|
329
|
+
const runtimeOverrideMockAdapter = createStreamingMock([
|
|
330
|
+
{
|
|
331
|
+
textContent: undefined,
|
|
332
|
+
toolCalls: [
|
|
333
|
+
{
|
|
334
|
+
id: "tool_1",
|
|
335
|
+
name: "test_tool",
|
|
336
|
+
input: {},
|
|
337
|
+
},
|
|
338
|
+
],
|
|
339
|
+
usage: { inputTokens: 10, outputTokens: 20 },
|
|
340
|
+
stopReason: "tool_use" as const,
|
|
341
|
+
},
|
|
342
|
+
]);
|
|
343
|
+
|
|
344
|
+
const toolProps: FlinkToolProps = {
|
|
345
|
+
id: "test_tool",
|
|
346
|
+
description: "Test tool",
|
|
347
|
+
inputSchema: z.object({}),
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const toolFn = async () => ({ success: true as const, data: {} });
|
|
351
|
+
const toolExecutor = new ToolExecutor(toolProps, toolFn, mockCtx);
|
|
352
|
+
const toolsMap = new Map([["test_tool", toolExecutor]]);
|
|
353
|
+
|
|
354
|
+
const agentProps: FlinkAgentProps<typeof mockCtx> = {
|
|
355
|
+
id: "test_agent",
|
|
356
|
+
description: "Test agent",
|
|
357
|
+
instructions: "You are helpful",
|
|
358
|
+
tools: ["test_tool"],
|
|
359
|
+
limits: { maxSteps: 10 },
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const llmAdapters = new Map();
|
|
363
|
+
llmAdapters.set("default", runtimeOverrideMockAdapter);
|
|
364
|
+
const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
|
|
365
|
+
|
|
366
|
+
const generator = runner.streamGenerator({
|
|
367
|
+
message: "Test",
|
|
368
|
+
options: { maxSteps: 2 },
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
for await (const chunk of generator) {
|
|
372
|
+
if (chunk.type === "complete") {
|
|
373
|
+
expect(chunk.result.stepsUsed).toBe(2);
|
|
374
|
+
expect(chunk.result.stoppedEarly).toBe(true);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
describe("Message format conversion", () => {
|
|
381
|
+
it("should convert string to message array", async () => {
|
|
382
|
+
const agentProps: FlinkAgentProps<typeof mockCtx> = {
|
|
383
|
+
id: "test_agent",
|
|
384
|
+
description: "Test agent",
|
|
385
|
+
instructions: "You are helpful",
|
|
386
|
+
tools: [],
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const toolsMap = new Map();
|
|
390
|
+
const llmAdapters = new Map();
|
|
391
|
+
llmAdapters.set("default", mockLLMAdapter);
|
|
392
|
+
const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
|
|
393
|
+
|
|
394
|
+
const generator = runner.streamGenerator({ message: "Hello" });
|
|
395
|
+
|
|
396
|
+
for await (const chunk of generator) {
|
|
397
|
+
if (chunk.type === "complete") {
|
|
398
|
+
expect(chunk.result).toBeDefined();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Verify LLM adapter was called with user message
|
|
403
|
+
expect(mockLLMAdapter.stream).toHaveBeenCalled();
|
|
404
|
+
const callArgs = (mockLLMAdapter.stream as jasmine.Spy).calls.first().args[0];
|
|
405
|
+
expect(callArgs.messages.length).toBeGreaterThanOrEqual(1);
|
|
406
|
+
expect(callArgs.messages[0]).toEqual({ role: "user", content: "Hello" });
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it("should convert Message[] to Anthropic format", async () => {
|
|
410
|
+
const agentProps: FlinkAgentProps<typeof mockCtx> = {
|
|
411
|
+
id: "test_agent",
|
|
412
|
+
description: "Test agent",
|
|
413
|
+
instructions: "You are helpful",
|
|
414
|
+
tools: [],
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const toolsMap = new Map();
|
|
418
|
+
const llmAdapters = new Map();
|
|
419
|
+
llmAdapters.set("default", mockLLMAdapter);
|
|
420
|
+
const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
|
|
421
|
+
|
|
422
|
+
const generator = runner.streamGenerator({
|
|
423
|
+
message: [
|
|
424
|
+
{ role: "user", content: "Hello" },
|
|
425
|
+
{ role: "user", content: "How are you?" },
|
|
426
|
+
],
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
for await (const chunk of generator) {
|
|
430
|
+
if (chunk.type === "complete") {
|
|
431
|
+
expect(chunk.result).toBeDefined();
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
expect(mockLLMAdapter.stream).toHaveBeenCalled();
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
describe("Model configuration", () => {
|
|
440
|
+
it("should pass configuration to LLM adapter", async () => {
|
|
441
|
+
const agentProps: FlinkAgentProps<typeof mockCtx> = {
|
|
442
|
+
id: "test_agent",
|
|
443
|
+
description: "Test agent",
|
|
444
|
+
instructions: "You are helpful",
|
|
445
|
+
tools: [],
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const toolsMap = new Map();
|
|
449
|
+
const llmAdapters = new Map();
|
|
450
|
+
llmAdapters.set("default", mockLLMAdapter);
|
|
451
|
+
const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
|
|
452
|
+
|
|
453
|
+
const generator = runner.streamGenerator({ message: "Hello" });
|
|
454
|
+
|
|
455
|
+
for await (const chunk of generator) {
|
|
456
|
+
if (chunk.type === "complete") {
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Verify LLM adapter was called via stream
|
|
462
|
+
expect(mockLLMAdapter.stream).toHaveBeenCalled();
|
|
463
|
+
const callArgs = (mockLLMAdapter.stream as jasmine.Spy).calls.mostRecent().args[0];
|
|
464
|
+
expect(callArgs.instructions).toBe("You are helpful");
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it("should pass custom temperature and maxTokens", async () => {
|
|
468
|
+
const agentProps: FlinkAgentProps<typeof mockCtx> = {
|
|
469
|
+
id: "test_agent",
|
|
470
|
+
description: "Test agent",
|
|
471
|
+
instructions: "You are helpful",
|
|
472
|
+
tools: [],
|
|
473
|
+
model: {
|
|
474
|
+
temperature: 0.3,
|
|
475
|
+
maxTokens: 2000,
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
const toolsMap = new Map();
|
|
480
|
+
const llmAdapters = new Map();
|
|
481
|
+
llmAdapters.set("default", mockLLMAdapter);
|
|
482
|
+
const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
|
|
483
|
+
|
|
484
|
+
const generator = runner.streamGenerator({ message: "Hello" });
|
|
485
|
+
|
|
486
|
+
for await (const chunk of generator) {
|
|
487
|
+
if (chunk.type === "complete") {
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const callArgs = (mockLLMAdapter.stream as jasmine.Spy).calls.mostRecent().args[0];
|
|
493
|
+
expect(callArgs.temperature).toBe(0.3);
|
|
494
|
+
expect(callArgs.maxTokens).toBe(2000);
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
describe("Error handling", () => {
|
|
499
|
+
it("should throw error when LLM adapter not configured", () => {
|
|
500
|
+
const agentProps: FlinkAgentProps<typeof mockCtx> = {
|
|
501
|
+
id: "test_agent",
|
|
502
|
+
description: "Test agent",
|
|
503
|
+
instructions: "You are helpful",
|
|
504
|
+
tools: [],
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
const toolsMap = new Map();
|
|
508
|
+
const llmAdapters = new Map(); // Empty map
|
|
509
|
+
|
|
510
|
+
expect(() => {
|
|
511
|
+
new AgentRunner(agentProps, toolsMap, llmAdapters);
|
|
512
|
+
}).toThrowError(/not configured/);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it("should throw error for unregistered adapter", () => {
|
|
516
|
+
const agentProps: FlinkAgentProps<typeof mockCtx> = {
|
|
517
|
+
id: "test_agent",
|
|
518
|
+
description: "Test agent",
|
|
519
|
+
instructions: "You are helpful",
|
|
520
|
+
tools: [],
|
|
521
|
+
model: {
|
|
522
|
+
adapterId: "openai",
|
|
523
|
+
},
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
const toolsMap = new Map();
|
|
527
|
+
const llmAdapters = new Map();
|
|
528
|
+
llmAdapters.set("default", mockLLMAdapter);
|
|
529
|
+
|
|
530
|
+
expect(() => {
|
|
531
|
+
new AgentRunner(agentProps, toolsMap, llmAdapters);
|
|
532
|
+
}).toThrowError(/LLM adapter "openai" not configured/);
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
describe("Message Conversion with Conversation History", () => {
|
|
537
|
+
it("should preserve assistant messages without tool calls", async () => {
|
|
538
|
+
const agentProps: FlinkAgentProps<typeof mockCtx> = {
|
|
539
|
+
id: "test_agent",
|
|
540
|
+
description: "Test agent",
|
|
541
|
+
instructions: "You are helpful",
|
|
542
|
+
tools: [],
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
const toolsMap = new Map();
|
|
546
|
+
const llmAdapters = new Map();
|
|
547
|
+
llmAdapters.set("default", mockLLMAdapter);
|
|
548
|
+
const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
|
|
549
|
+
|
|
550
|
+
const history = [
|
|
551
|
+
{ role: "user" as const, content: "Who are you?" },
|
|
552
|
+
{ role: "assistant" as const, content: "I'm a weather assistant." },
|
|
553
|
+
];
|
|
554
|
+
|
|
555
|
+
const generator = runner.streamGenerator({
|
|
556
|
+
message: "What's the weather?",
|
|
557
|
+
history,
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
for await (const chunk of generator) {
|
|
561
|
+
if (chunk.type === "complete") {
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Verify LLM adapter was called with history preserved
|
|
567
|
+
expect(mockLLMAdapter.stream).toHaveBeenCalled();
|
|
568
|
+
const callArgs = (mockLLMAdapter.stream as jasmine.Spy).calls.first().args[0];
|
|
569
|
+
const messages = callArgs.messages;
|
|
570
|
+
|
|
571
|
+
// Note: messages array gets mutated after LLM call (assistant message added)
|
|
572
|
+
// So we check that the first 3 messages are correct (history + new user message)
|
|
573
|
+
expect(messages.length).toBeGreaterThanOrEqual(3);
|
|
574
|
+
expect(messages[0]).toEqual({ role: "user", content: "Who are you?" });
|
|
575
|
+
expect(messages[1]).toEqual({ role: "assistant", content: "I'm a weather assistant." });
|
|
576
|
+
expect(messages[2]).toEqual({ role: "user", content: "What's the weather?" });
|
|
577
|
+
|
|
578
|
+
// Verify assistant message from history was preserved (not filtered out)
|
|
579
|
+
expect(messages[1].role).toBe("assistant");
|
|
580
|
+
expect(messages[1].content).toBe("I'm a weather assistant.");
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it("should preserve assistant messages with tool calls", async () => {
|
|
584
|
+
const agentProps: FlinkAgentProps<typeof mockCtx> = {
|
|
585
|
+
id: "test_agent",
|
|
586
|
+
description: "Test agent",
|
|
587
|
+
instructions: "You are helpful",
|
|
588
|
+
tools: [],
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
const toolsMap = new Map();
|
|
592
|
+
const llmAdapters = new Map();
|
|
593
|
+
llmAdapters.set("default", mockLLMAdapter);
|
|
594
|
+
const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
|
|
595
|
+
|
|
596
|
+
const history = [
|
|
597
|
+
{ role: "user" as const, content: "What's the weather in Stockholm?" },
|
|
598
|
+
{
|
|
599
|
+
role: "assistant" as const,
|
|
600
|
+
content: "",
|
|
601
|
+
toolCalls: [{ id: "1", name: "get-weather", input: { city: "Stockholm" } }],
|
|
602
|
+
},
|
|
603
|
+
{ role: "tool" as const, toolCallId: "1", toolName: "get-weather", result: "22°C, Sunny" },
|
|
604
|
+
];
|
|
605
|
+
|
|
606
|
+
const generator = runner.streamGenerator({
|
|
607
|
+
message: "And tomorrow?",
|
|
608
|
+
history,
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
for await (const chunk of generator) {
|
|
612
|
+
if (chunk.type === "complete") {
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
expect(mockLLMAdapter.stream).toHaveBeenCalled();
|
|
618
|
+
const callArgs = (mockLLMAdapter.stream as jasmine.Spy).calls.first().args[0];
|
|
619
|
+
const messages = callArgs.messages;
|
|
620
|
+
|
|
621
|
+
// Note: messages array gets mutated after LLM call
|
|
622
|
+
// Check first 4 messages: history (3) + new user message (1)
|
|
623
|
+
expect(messages.length).toBeGreaterThanOrEqual(4);
|
|
624
|
+
expect(messages[0].role).toBe("user");
|
|
625
|
+
expect(messages[1].role).toBe("assistant");
|
|
626
|
+
expect(messages[2].role).toBe("user"); // Tool result
|
|
627
|
+
expect(messages[3].role).toBe("user"); // New message
|
|
628
|
+
|
|
629
|
+
// Assistant message should have tool_use content blocks (preserved from history)
|
|
630
|
+
expect(Array.isArray(messages[1].content)).toBe(true);
|
|
631
|
+
const contentBlocks = messages[1].content as any[];
|
|
632
|
+
expect(contentBlocks.length).toBe(1);
|
|
633
|
+
expect(contentBlocks[0].type).toBe("tool_use");
|
|
634
|
+
expect(contentBlocks[0].name).toBe("get-weather");
|
|
635
|
+
expect(contentBlocks[0].input).toEqual({ city: "Stockholm" });
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
it("should maintain alternating user/assistant pattern", async () => {
|
|
639
|
+
const agentProps: FlinkAgentProps<typeof mockCtx> = {
|
|
640
|
+
id: "test_agent",
|
|
641
|
+
description: "Test agent",
|
|
642
|
+
instructions: "You are helpful",
|
|
643
|
+
tools: [],
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
const toolsMap = new Map();
|
|
647
|
+
const llmAdapters = new Map();
|
|
648
|
+
llmAdapters.set("default", mockLLMAdapter);
|
|
649
|
+
const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
|
|
650
|
+
|
|
651
|
+
const history = [
|
|
652
|
+
{ role: "user" as const, content: "Hello" },
|
|
653
|
+
{ role: "assistant" as const, content: "Hi there!" },
|
|
654
|
+
{ role: "user" as const, content: "How are you?" },
|
|
655
|
+
{ role: "assistant" as const, content: "I'm doing well!" },
|
|
656
|
+
];
|
|
657
|
+
|
|
658
|
+
const generator = runner.streamGenerator({
|
|
659
|
+
message: "What's your name?",
|
|
660
|
+
history,
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
for await (const chunk of generator) {
|
|
664
|
+
if (chunk.type === "complete") {
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
expect(mockLLMAdapter.stream).toHaveBeenCalled();
|
|
670
|
+
const callArgs = (mockLLMAdapter.stream as jasmine.Spy).calls.first().args[0];
|
|
671
|
+
const messages = callArgs.messages;
|
|
672
|
+
|
|
673
|
+
// Note: messages array gets mutated after LLM call
|
|
674
|
+
// Check first 5 messages: history (4) + new user message (1)
|
|
675
|
+
expect(messages.length).toBeGreaterThanOrEqual(5);
|
|
676
|
+
|
|
677
|
+
// Verify no consecutive user or assistant messages in the first 5 (input messages)
|
|
678
|
+
for (let i = 1; i < Math.min(5, messages.length); i++) {
|
|
679
|
+
expect(messages[i].role).not.toBe(messages[i - 1].role);
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
it("should handle assistant messages with both text and tool calls", async () => {
|
|
684
|
+
const agentProps: FlinkAgentProps<typeof mockCtx> = {
|
|
685
|
+
id: "test_agent",
|
|
686
|
+
description: "Test agent",
|
|
687
|
+
instructions: "You are helpful",
|
|
688
|
+
tools: [],
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
const toolsMap = new Map();
|
|
692
|
+
const llmAdapters = new Map();
|
|
693
|
+
llmAdapters.set("default", mockLLMAdapter);
|
|
694
|
+
const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
|
|
695
|
+
|
|
696
|
+
const history = [
|
|
697
|
+
{ role: "user" as const, content: "Check the weather" },
|
|
698
|
+
{
|
|
699
|
+
role: "assistant" as const,
|
|
700
|
+
content: "Let me check that for you",
|
|
701
|
+
toolCalls: [{ id: "1", name: "get-weather", input: { city: "Stockholm" } }],
|
|
702
|
+
},
|
|
703
|
+
];
|
|
704
|
+
|
|
705
|
+
const generator = runner.streamGenerator({ message: history });
|
|
706
|
+
|
|
707
|
+
for await (const chunk of generator) {
|
|
708
|
+
if (chunk.type === "complete") {
|
|
709
|
+
break;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
expect(mockLLMAdapter.stream).toHaveBeenCalled();
|
|
714
|
+
const callArgs = (mockLLMAdapter.stream as jasmine.Spy).calls.first().args[0];
|
|
715
|
+
const messages = callArgs.messages;
|
|
716
|
+
|
|
717
|
+
// Assistant message should have both text and tool_use blocks
|
|
718
|
+
expect(Array.isArray(messages[1].content)).toBe(true);
|
|
719
|
+
const contentBlocks = messages[1].content as any[];
|
|
720
|
+
expect(contentBlocks.length).toBe(2);
|
|
721
|
+
expect(contentBlocks[0].type).toBe("text");
|
|
722
|
+
expect(contentBlocks[0].text).toBe("Let me check that for you");
|
|
723
|
+
expect(contentBlocks[1].type).toBe("tool_use");
|
|
724
|
+
expect(contentBlocks[1].name).toBe("get-weather");
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
it("should convert tool messages to user messages with results", async () => {
|
|
728
|
+
const agentProps: FlinkAgentProps<typeof mockCtx> = {
|
|
729
|
+
id: "test_agent",
|
|
730
|
+
description: "Test agent",
|
|
731
|
+
instructions: "You are helpful",
|
|
732
|
+
tools: [],
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
const toolsMap = new Map();
|
|
736
|
+
const llmAdapters = new Map();
|
|
737
|
+
llmAdapters.set("default", mockLLMAdapter);
|
|
738
|
+
const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
|
|
739
|
+
|
|
740
|
+
const history = [
|
|
741
|
+
{ role: "user" as const, content: "Check weather" },
|
|
742
|
+
{
|
|
743
|
+
role: "assistant" as const,
|
|
744
|
+
content: "",
|
|
745
|
+
toolCalls: [{ id: "1", name: "get-weather", input: { city: "Stockholm" } }],
|
|
746
|
+
},
|
|
747
|
+
{ role: "tool" as const, toolCallId: "1", toolName: "get-weather", result: "22°C" },
|
|
748
|
+
];
|
|
749
|
+
|
|
750
|
+
const generator = runner.streamGenerator({ message: history });
|
|
751
|
+
|
|
752
|
+
for await (const chunk of generator) {
|
|
753
|
+
if (chunk.type === "complete") {
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
expect(mockLLMAdapter.stream).toHaveBeenCalled();
|
|
759
|
+
const callArgs = (mockLLMAdapter.stream as jasmine.Spy).calls.first().args[0];
|
|
760
|
+
const messages = callArgs.messages;
|
|
761
|
+
|
|
762
|
+
// Tool message should be converted to user message with tool_result content block
|
|
763
|
+
expect(messages[2].role).toBe("user");
|
|
764
|
+
expect(Array.isArray(messages[2].content)).toBe(true);
|
|
765
|
+
const contentBlocks = messages[2].content as any[];
|
|
766
|
+
expect(contentBlocks.length).toBe(1);
|
|
767
|
+
expect(contentBlocks[0].type).toBe("tool_result");
|
|
768
|
+
expect(contentBlocks[0].tool_use_id).toBe("1");
|
|
769
|
+
expect(contentBlocks[0].content).toBe("22°C");
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
it("should handle empty content with tool calls", async () => {
|
|
773
|
+
const agentProps: FlinkAgentProps<typeof mockCtx> = {
|
|
774
|
+
id: "test_agent",
|
|
775
|
+
description: "Test agent",
|
|
776
|
+
instructions: "You are helpful",
|
|
777
|
+
tools: [],
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
const toolsMap = new Map();
|
|
781
|
+
const llmAdapters = new Map();
|
|
782
|
+
llmAdapters.set("default", mockLLMAdapter);
|
|
783
|
+
const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
|
|
784
|
+
|
|
785
|
+
const history = [
|
|
786
|
+
{ role: "user" as const, content: "Check weather" },
|
|
787
|
+
{
|
|
788
|
+
role: "assistant" as const,
|
|
789
|
+
content: "", // Empty content
|
|
790
|
+
toolCalls: [{ id: "1", name: "get-weather", input: {} }],
|
|
791
|
+
},
|
|
792
|
+
];
|
|
793
|
+
|
|
794
|
+
const generator = runner.streamGenerator({ message: history });
|
|
795
|
+
|
|
796
|
+
for await (const chunk of generator) {
|
|
797
|
+
if (chunk.type === "complete") {
|
|
798
|
+
break;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
expect(mockLLMAdapter.stream).toHaveBeenCalled();
|
|
803
|
+
const callArgs = (mockLLMAdapter.stream as jasmine.Spy).calls.first().args[0];
|
|
804
|
+
const messages = callArgs.messages;
|
|
805
|
+
|
|
806
|
+
// Assistant message should only have tool_use block (no text block for empty content)
|
|
807
|
+
expect(Array.isArray(messages[1].content)).toBe(true);
|
|
808
|
+
const contentBlocks = messages[1].content as any[];
|
|
809
|
+
expect(contentBlocks.length).toBe(1);
|
|
810
|
+
expect(contentBlocks[0].type).toBe("tool_use");
|
|
811
|
+
});
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
describe("Dynamic Instructions", () => {
|
|
815
|
+
it("should use static string instructions (backwards compatibility)", async () => {
|
|
816
|
+
const agentProps: FlinkAgentProps<typeof mockCtx> = {
|
|
817
|
+
id: "test_agent",
|
|
818
|
+
description: "Test agent",
|
|
819
|
+
instructions: "Static instructions",
|
|
820
|
+
tools: [],
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
const toolsMap = new Map();
|
|
824
|
+
const llmAdapters = new Map();
|
|
825
|
+
llmAdapters.set("default", mockLLMAdapter);
|
|
826
|
+
const runner = new AgentRunner(agentProps, toolsMap, llmAdapters, "test_agent", mockCtx);
|
|
827
|
+
|
|
828
|
+
const generator = runner.streamGenerator({ message: "Hello" });
|
|
829
|
+
|
|
830
|
+
for await (const chunk of generator) {
|
|
831
|
+
if (chunk.type === "complete") {
|
|
832
|
+
break;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Verify LLM was called with static instructions
|
|
837
|
+
expect(mockLLMAdapter.stream).toHaveBeenCalled();
|
|
838
|
+
const callArgs = (mockLLMAdapter.stream as jasmine.Spy).calls.first().args[0];
|
|
839
|
+
expect(callArgs.instructions).toBe("Static instructions");
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
it("should resolve synchronous callback instructions", async () => {
|
|
843
|
+
const instructionsCallback = jasmine.createSpy("instructionsCallback").and.returnValue("Dynamic: test-user");
|
|
844
|
+
|
|
845
|
+
const agentProps: FlinkAgentProps<typeof mockCtx> = {
|
|
846
|
+
id: "test_agent",
|
|
847
|
+
description: "Test agent",
|
|
848
|
+
instructions: instructionsCallback,
|
|
849
|
+
tools: [],
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
const toolsMap = new Map();
|
|
853
|
+
const llmAdapters = new Map();
|
|
854
|
+
llmAdapters.set("default", mockLLMAdapter);
|
|
855
|
+
const runner = new AgentRunner(agentProps, toolsMap, llmAdapters, "test_agent", mockCtx);
|
|
856
|
+
|
|
857
|
+
const generator = runner.streamGenerator({
|
|
858
|
+
message: "Hello",
|
|
859
|
+
user: { id: "123", name: "test-user" },
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
for await (const chunk of generator) {
|
|
863
|
+
if (chunk.type === "complete") {
|
|
864
|
+
break;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// Verify callback was called with correct context
|
|
869
|
+
expect(instructionsCallback).toHaveBeenCalledTimes(1);
|
|
870
|
+
const callArgs = instructionsCallback.calls.first().args;
|
|
871
|
+
expect(callArgs[0]).toBe(mockCtx); // ctx
|
|
872
|
+
expect(callArgs[1].user).toEqual({ id: "123", name: "test-user" }); // agentContext
|
|
873
|
+
|
|
874
|
+
// Verify LLM was called with resolved instructions
|
|
875
|
+
expect(mockLLMAdapter.stream).toHaveBeenCalled();
|
|
876
|
+
const llmCallArgs = (mockLLMAdapter.stream as jasmine.Spy).calls.first().args[0];
|
|
877
|
+
expect(llmCallArgs.instructions).toBe("Dynamic: test-user");
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
it("should resolve async callback instructions", async () => {
|
|
881
|
+
const mockUser = { id: "123", name: "test-user", tier: "premium" };
|
|
882
|
+
const mockRepoCtx = {
|
|
883
|
+
repos: {
|
|
884
|
+
userRepo: {
|
|
885
|
+
getById: jasmine.createSpy("getById").and.returnValue(Promise.resolve(mockUser)),
|
|
886
|
+
},
|
|
887
|
+
},
|
|
888
|
+
plugins: {},
|
|
889
|
+
};
|
|
890
|
+
|
|
891
|
+
const instructionsCallback = async (ctx: any, agentContext: any) => {
|
|
892
|
+
const profile = await ctx.repos.userRepo.getById(agentContext.user.id);
|
|
893
|
+
return `You are a support agent. Customer: ${profile.name}, Tier: ${profile.tier}`;
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
const agentProps: FlinkAgentProps<typeof mockCtx> = {
|
|
897
|
+
id: "test_agent",
|
|
898
|
+
description: "Test agent",
|
|
899
|
+
instructions: instructionsCallback,
|
|
900
|
+
tools: [],
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
const toolsMap = new Map();
|
|
904
|
+
const llmAdapters = new Map();
|
|
905
|
+
llmAdapters.set("default", mockLLMAdapter);
|
|
906
|
+
const runner = new AgentRunner(agentProps, toolsMap, llmAdapters, "test_agent", mockRepoCtx);
|
|
907
|
+
|
|
908
|
+
const generator = runner.streamGenerator({
|
|
909
|
+
message: "Hello",
|
|
910
|
+
user: { id: "123" },
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
for await (const chunk of generator) {
|
|
914
|
+
if (chunk.type === "complete") {
|
|
915
|
+
break;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Verify repo was called
|
|
920
|
+
expect(mockRepoCtx.repos.userRepo.getById).toHaveBeenCalledWith("123");
|
|
921
|
+
|
|
922
|
+
// Verify LLM was called with resolved instructions
|
|
923
|
+
expect(mockLLMAdapter.stream).toHaveBeenCalled();
|
|
924
|
+
const llmCallArgs = (mockLLMAdapter.stream as jasmine.Spy).calls.first().args[0];
|
|
925
|
+
expect(llmCallArgs.instructions).toBe("You are a support agent. Customer: test-user, Tier: premium");
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
it("should pass correct context to callback", async () => {
|
|
929
|
+
const instructionsCallback = jasmine.createSpy("instructionsCallback").and.returnValue("Instructions");
|
|
930
|
+
|
|
931
|
+
const agentProps: FlinkAgentProps<typeof mockCtx> = {
|
|
932
|
+
id: "test_agent",
|
|
933
|
+
description: "Test agent",
|
|
934
|
+
instructions: instructionsCallback,
|
|
935
|
+
tools: [],
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
const toolsMap = new Map();
|
|
939
|
+
const llmAdapters = new Map();
|
|
940
|
+
llmAdapters.set("default", mockLLMAdapter);
|
|
941
|
+
const runner = new AgentRunner(agentProps, toolsMap, llmAdapters, "test_agent", mockCtx);
|
|
942
|
+
|
|
943
|
+
const generator = runner.streamGenerator({
|
|
944
|
+
message: "Hello",
|
|
945
|
+
user: { id: "123" },
|
|
946
|
+
conversationId: "conv-456",
|
|
947
|
+
metadata: { source: "handler" },
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
for await (const chunk of generator) {
|
|
951
|
+
if (chunk.type === "complete") {
|
|
952
|
+
break;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// Verify callback received correct context
|
|
957
|
+
expect(instructionsCallback).toHaveBeenCalledTimes(1);
|
|
958
|
+
const callArgs = instructionsCallback.calls.first().args;
|
|
959
|
+
|
|
960
|
+
// Check ctx parameter
|
|
961
|
+
expect(callArgs[0]).toBe(mockCtx);
|
|
962
|
+
|
|
963
|
+
// Check execContext parameter
|
|
964
|
+
const execContext = callArgs[1];
|
|
965
|
+
expect(execContext.agentId).toBe("test_agent");
|
|
966
|
+
expect(execContext.conversationId).toBe("conv-456");
|
|
967
|
+
expect(execContext.user).toEqual({ id: "123" });
|
|
968
|
+
expect(execContext.metadata).toEqual({ source: "handler" });
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
it("should handle callback errors gracefully", async () => {
|
|
972
|
+
const instructionsCallback = () => {
|
|
973
|
+
throw new Error("Database connection failed");
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
const agentProps: FlinkAgentProps<typeof mockCtx> = {
|
|
977
|
+
id: "test_agent",
|
|
978
|
+
description: "Test agent",
|
|
979
|
+
instructions: instructionsCallback,
|
|
980
|
+
tools: [],
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
const toolsMap = new Map();
|
|
984
|
+
const llmAdapters = new Map();
|
|
985
|
+
llmAdapters.set("default", mockLLMAdapter);
|
|
986
|
+
const runner = new AgentRunner(agentProps, toolsMap, llmAdapters, "test_agent", mockCtx);
|
|
987
|
+
|
|
988
|
+
const generator = runner.streamGenerator({ message: "Hello" });
|
|
989
|
+
|
|
990
|
+
try {
|
|
991
|
+
for await (const chunk of generator) {
|
|
992
|
+
// Should not reach here
|
|
993
|
+
}
|
|
994
|
+
fail("Expected error to be thrown");
|
|
995
|
+
} catch (err: any) {
|
|
996
|
+
expect(err.message).toContain("Failed to resolve instructions for agent test_agent");
|
|
997
|
+
expect(err.message).toContain("Database connection failed");
|
|
998
|
+
}
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
it("should resolve instructions once per execution", async () => {
|
|
1002
|
+
let callCount = 0;
|
|
1003
|
+
const instructionsCallback = () => {
|
|
1004
|
+
callCount++;
|
|
1005
|
+
return "Instructions";
|
|
1006
|
+
};
|
|
1007
|
+
|
|
1008
|
+
// Mock adapter that calls LLM 3 times (3 steps with tool calls)
|
|
1009
|
+
const multiStepMockAdapter = createStreamingMock([
|
|
1010
|
+
{
|
|
1011
|
+
textContent: undefined,
|
|
1012
|
+
toolCalls: [{ id: "tool_1", name: "test_tool", input: {} }],
|
|
1013
|
+
usage: { inputTokens: 10, outputTokens: 20 },
|
|
1014
|
+
stopReason: "tool_use" as const,
|
|
1015
|
+
},
|
|
1016
|
+
{
|
|
1017
|
+
textContent: undefined,
|
|
1018
|
+
toolCalls: [{ id: "tool_2", name: "test_tool", input: {} }],
|
|
1019
|
+
usage: { inputTokens: 10, outputTokens: 20 },
|
|
1020
|
+
stopReason: "tool_use" as const,
|
|
1021
|
+
},
|
|
1022
|
+
{
|
|
1023
|
+
textContent: "Final response",
|
|
1024
|
+
toolCalls: [],
|
|
1025
|
+
usage: { inputTokens: 10, outputTokens: 20 },
|
|
1026
|
+
stopReason: "end_turn" as const,
|
|
1027
|
+
},
|
|
1028
|
+
]);
|
|
1029
|
+
|
|
1030
|
+
const toolProps: FlinkToolProps = {
|
|
1031
|
+
id: "test_tool",
|
|
1032
|
+
description: "Test tool",
|
|
1033
|
+
inputSchema: z.object({}),
|
|
1034
|
+
};
|
|
1035
|
+
const toolFn = async () => ({ success: true as const, data: {} });
|
|
1036
|
+
const toolExecutor = new ToolExecutor(toolProps, toolFn, mockCtx);
|
|
1037
|
+
const toolsMap = new Map([["test_tool", toolExecutor]]);
|
|
1038
|
+
|
|
1039
|
+
const agentProps: FlinkAgentProps<typeof mockCtx> = {
|
|
1040
|
+
id: "test_agent",
|
|
1041
|
+
description: "Test agent",
|
|
1042
|
+
instructions: instructionsCallback,
|
|
1043
|
+
tools: ["test_tool"],
|
|
1044
|
+
};
|
|
1045
|
+
|
|
1046
|
+
const llmAdapters = new Map();
|
|
1047
|
+
llmAdapters.set("default", multiStepMockAdapter);
|
|
1048
|
+
const runner = new AgentRunner(agentProps, toolsMap, llmAdapters, "test_agent", mockCtx);
|
|
1049
|
+
|
|
1050
|
+
const generator = runner.streamGenerator({ message: "Hello" });
|
|
1051
|
+
|
|
1052
|
+
for await (const chunk of generator) {
|
|
1053
|
+
if (chunk.type === "complete") {
|
|
1054
|
+
expect(chunk.result.stepsUsed).toBe(3);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// Callback should be called ONCE, not 3 times
|
|
1059
|
+
expect(callCount).toBe(1);
|
|
1060
|
+
});
|
|
1061
|
+
});
|
|
1062
|
+
});
|