@flink-app/flink 0.14.3 → 2.0.0-alpha.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +66 -0
- package/cli/build.ts +8 -1
- package/cli/run.ts +8 -1
- package/dist/cli/build.js +8 -1
- package/dist/cli/run.js +8 -1
- package/dist/src/FlinkApp.d.ts +33 -0
- package/dist/src/FlinkApp.js +279 -35
- package/dist/src/FlinkContext.d.ts +21 -0
- package/dist/src/FlinkHttpHandler.d.ts +152 -9
- package/dist/src/FlinkHttpHandler.js +37 -1
- package/dist/src/TypeScriptCompiler.d.ts +42 -0
- package/dist/src/TypeScriptCompiler.js +346 -4
- package/dist/src/TypeScriptUtils.js +4 -0
- package/dist/src/ai/AgentRunner.d.ts +39 -0
- package/dist/src/ai/AgentRunner.js +625 -0
- package/dist/src/ai/FlinkAgent.d.ts +446 -0
- package/dist/src/ai/FlinkAgent.js +633 -0
- package/dist/src/ai/FlinkTool.d.ts +37 -0
- package/dist/src/ai/FlinkTool.js +2 -0
- package/dist/src/ai/LLMAdapter.d.ts +119 -0
- package/dist/src/ai/LLMAdapter.js +2 -0
- package/dist/src/ai/SubAgentExecutor.d.ts +36 -0
- package/dist/src/ai/SubAgentExecutor.js +220 -0
- package/dist/src/ai/ToolExecutor.d.ts +35 -0
- package/dist/src/ai/ToolExecutor.js +237 -0
- package/dist/src/ai/index.d.ts +5 -0
- package/dist/src/ai/index.js +21 -0
- package/dist/src/handlers/StreamWriterFactory.d.ts +20 -0
- package/dist/src/handlers/StreamWriterFactory.js +83 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +4 -0
- package/dist/src/utils.d.ts +30 -0
- package/dist/src/utils.js +52 -0
- package/package.json +16 -2
- package/readme.md +425 -0
- package/spec/AgentDuplicateDetection.spec.ts +112 -0
- package/spec/AgentRunner.spec.ts +527 -0
- package/spec/ConversationHooks.spec.ts +290 -0
- package/spec/FlinkAgent.spec.ts +310 -0
- package/spec/FlinkApp.onError.spec.ts +1 -2
- package/spec/FlinkApp.query.spec.ts +107 -0
- package/spec/FlinkApp.validationMode.spec.ts +155 -0
- package/spec/StreamingIntegration.spec.ts +138 -0
- package/spec/SubAgentSupport.spec.ts +941 -0
- package/spec/ToolExecutor.spec.ts +360 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar.js +57 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar2.js +59 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema.js +53 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema2.js +53 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema3.js +53 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema.js +55 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema2.js +55 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile.js +58 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile2.js +58 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler.js +53 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler2.js +55 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchCar.js +58 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOnboardingSession.js +76 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOrderWithComplexTypes.js +58 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchProductWithIntersection.js +59 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchUserWithUnion.js +59 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PostCar.js +55 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogin.js +56 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogout.js +55 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PutCar.js +55 -0
- package/spec/mock-project/dist/spec/mock-project/src/index.js +83 -0
- package/spec/mock-project/dist/spec/mock-project/src/repos/CarRepo.js +26 -0
- package/spec/mock-project/dist/spec/mock-project/src/schemas/Car.js +2 -0
- package/spec/mock-project/dist/spec/mock-project/src/schemas/DefaultExportSchema.js +2 -0
- package/spec/mock-project/dist/spec/mock-project/src/schemas/FileWithTwoSchemas.js +2 -0
- package/spec/mock-project/dist/src/FlinkApp.js +1012 -0
- package/spec/mock-project/dist/src/FlinkContext.js +2 -0
- package/spec/mock-project/dist/src/FlinkErrors.js +143 -0
- package/spec/mock-project/dist/src/FlinkHttpHandler.js +47 -0
- package/spec/mock-project/dist/src/FlinkJob.js +2 -0
- package/spec/mock-project/dist/src/FlinkLog.js +26 -0
- package/spec/mock-project/dist/src/FlinkPlugin.js +2 -0
- package/spec/mock-project/dist/src/FlinkRepo.js +224 -0
- package/spec/mock-project/dist/src/FlinkResponse.js +2 -0
- package/spec/mock-project/dist/src/ai/AgentExecutor.js +279 -0
- package/spec/mock-project/dist/src/ai/AgentRunner.js +625 -0
- package/spec/mock-project/dist/src/ai/FlinkAgent.js +633 -0
- package/spec/mock-project/dist/src/ai/FlinkTool.js +2 -0
- package/spec/mock-project/dist/src/ai/LLMAdapter.js +2 -0
- package/spec/mock-project/dist/src/ai/SubAgentExecutor.js +220 -0
- package/spec/mock-project/dist/src/ai/ToolExecutor.js +237 -0
- package/spec/mock-project/dist/src/auth/FlinkAuthPlugin.js +2 -0
- package/spec/mock-project/dist/src/auth/FlinkAuthUser.js +2 -0
- package/spec/mock-project/dist/src/handlers/StreamWriterFactory.js +83 -0
- package/spec/mock-project/dist/src/index.js +17 -69
- package/spec/mock-project/dist/src/mock-data-generator.js +9 -0
- package/spec/mock-project/dist/src/utils.js +290 -0
- package/spec/mock-project/tsconfig.json +6 -1
- package/spec/testHelpers.ts +49 -0
- package/spec/utils.caseConversion.spec.ts +80 -0
- package/spec/utils.spec.ts +13 -13
- package/src/FlinkApp.ts +275 -8
- package/src/FlinkContext.ts +22 -0
- package/src/FlinkHttpHandler.ts +164 -10
- package/src/TypeScriptCompiler.ts +398 -7
- package/src/TypeScriptUtils.ts +5 -0
- package/src/ai/AgentRunner.ts +549 -0
- package/src/ai/FlinkAgent.ts +770 -0
- package/src/ai/FlinkTool.ts +40 -0
- package/src/ai/LLMAdapter.ts +96 -0
- package/src/ai/SubAgentExecutor.ts +199 -0
- package/src/ai/ToolExecutor.ts +193 -0
- package/src/ai/index.ts +5 -0
- package/src/handlers/StreamWriterFactory.ts +84 -0
- package/src/index.ts +4 -0
- package/src/utils.ts +52 -0
- package/tsconfig.json +6 -1
|
@@ -0,0 +1,527 @@
|
|
|
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
|
+
textContent: "Test response",
|
|
23
|
+
toolCalls: [],
|
|
24
|
+
usage: { inputTokens: 10, outputTokens: 20 },
|
|
25
|
+
stopReason: "end_turn" as const,
|
|
26
|
+
}]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("Basic execution", () => {
|
|
30
|
+
it("should execute simple agent without tools", async () => {
|
|
31
|
+
const agentProps: FlinkAgentProps = {
|
|
32
|
+
id: "test_agent",
|
|
33
|
+
description: "Test agent",
|
|
34
|
+
instructions: "You are a helpful assistant",
|
|
35
|
+
tools: [],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const toolsMap = new Map();
|
|
39
|
+
const llmAdapters = new Map();
|
|
40
|
+
llmAdapters.set("default", mockLLMAdapter);
|
|
41
|
+
const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
|
|
42
|
+
|
|
43
|
+
const generator = runner.streamGenerator({ message: "Hello" });
|
|
44
|
+
const chunks: any[] = [];
|
|
45
|
+
|
|
46
|
+
for await (const chunk of generator) {
|
|
47
|
+
chunks.push(chunk);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Should have complete event
|
|
51
|
+
expect(chunks.length).toBeGreaterThan(0);
|
|
52
|
+
const completeChunk = chunks.find((c) => c.type === "complete");
|
|
53
|
+
expect(completeChunk).toBeDefined();
|
|
54
|
+
expect(completeChunk.result.message).toBe("Test response");
|
|
55
|
+
expect(completeChunk.result.stepsUsed).toBeGreaterThan(0);
|
|
56
|
+
expect(completeChunk.result.toolCalls).toEqual([]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should track token usage", async () => {
|
|
60
|
+
const agentProps: FlinkAgentProps = {
|
|
61
|
+
id: "test_agent",
|
|
62
|
+
description: "Test agent",
|
|
63
|
+
instructions: "You are a helpful assistant",
|
|
64
|
+
tools: [],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const toolsMap = new Map();
|
|
68
|
+
const llmAdapters = new Map();
|
|
69
|
+
llmAdapters.set("default", mockLLMAdapter);
|
|
70
|
+
const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
|
|
71
|
+
|
|
72
|
+
const generator = runner.streamGenerator({ message: "Hello" });
|
|
73
|
+
|
|
74
|
+
for await (const chunk of generator) {
|
|
75
|
+
if (chunk.type === "complete") {
|
|
76
|
+
expect(chunk.result.usage).toBeDefined();
|
|
77
|
+
expect(chunk.result.usage?.inputTokens).toBeGreaterThan(0);
|
|
78
|
+
expect(chunk.result.usage?.outputTokens).toBeGreaterThan(0);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("Tool calling", () => {
|
|
85
|
+
it("should execute tool calls", async () => {
|
|
86
|
+
// Create mock tool
|
|
87
|
+
const toolProps: FlinkToolProps = {
|
|
88
|
+
id: "get_weather",
|
|
89
|
+
description: "Get weather",
|
|
90
|
+
inputSchema: z.object({ city: z.string() }),
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const toolFn = jasmine.createSpy("toolFn").and.returnValue(
|
|
94
|
+
Promise.resolve({
|
|
95
|
+
success: true,
|
|
96
|
+
data: { temperature: 22, conditions: "sunny" },
|
|
97
|
+
})
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const toolExecutor = new ToolExecutor(toolProps, toolFn as any, mockCtx);
|
|
101
|
+
const toolsMap = new Map([["get_weather", toolExecutor]]);
|
|
102
|
+
|
|
103
|
+
// Mock LLM adapter response with tool call
|
|
104
|
+
const weatherMockAdapter = createStreamingMock([
|
|
105
|
+
// First call: agent requests tool
|
|
106
|
+
{
|
|
107
|
+
textContent: "Let me check the weather",
|
|
108
|
+
toolCalls: [
|
|
109
|
+
{
|
|
110
|
+
id: "tool_1",
|
|
111
|
+
name: "get_weather",
|
|
112
|
+
input: { city: "Stockholm" },
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
usage: { inputTokens: 10, outputTokens: 20 },
|
|
116
|
+
stopReason: "tool_use" as const,
|
|
117
|
+
},
|
|
118
|
+
// Second call: agent responds with result
|
|
119
|
+
{
|
|
120
|
+
textContent: "It's sunny and 22°C",
|
|
121
|
+
toolCalls: [],
|
|
122
|
+
usage: { inputTokens: 15, outputTokens: 10 },
|
|
123
|
+
stopReason: "end_turn" as const,
|
|
124
|
+
}
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
const agentProps: FlinkAgentProps = {
|
|
128
|
+
id: "weather_agent",
|
|
129
|
+
description: "Weather assistant",
|
|
130
|
+
instructions: "You help with weather",
|
|
131
|
+
tools: ["get_weather"],
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const llmAdapters = new Map();
|
|
135
|
+
llmAdapters.set("default", weatherMockAdapter);
|
|
136
|
+
const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
|
|
137
|
+
|
|
138
|
+
const generator = runner.streamGenerator({ message: "What's the weather in Stockholm?" });
|
|
139
|
+
|
|
140
|
+
for await (const chunk of generator) {
|
|
141
|
+
if (chunk.type === "complete") {
|
|
142
|
+
expect(chunk.result.message).toBe("It's sunny and 22°C");
|
|
143
|
+
expect(chunk.result.toolCalls.length).toBe(1);
|
|
144
|
+
expect(chunk.result.toolCalls[0].name).toBe("get_weather");
|
|
145
|
+
expect(chunk.result.toolCalls[0].input).toEqual({ city: "Stockholm" });
|
|
146
|
+
expect(chunk.result.toolCalls[0].output).toEqual({
|
|
147
|
+
temperature: 22,
|
|
148
|
+
conditions: "sunny",
|
|
149
|
+
});
|
|
150
|
+
expect(chunk.result.stepsUsed).toBe(2);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Verify tool was called
|
|
155
|
+
expect(toolFn).toHaveBeenCalledWith({
|
|
156
|
+
input: { city: "Stockholm" },
|
|
157
|
+
ctx: mockCtx,
|
|
158
|
+
user: undefined,
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("should handle tool errors gracefully", async () => {
|
|
163
|
+
// Create mock tool that returns error
|
|
164
|
+
const toolProps: FlinkToolProps = {
|
|
165
|
+
id: "get_weather",
|
|
166
|
+
description: "Get weather",
|
|
167
|
+
inputSchema: z.object({ city: z.string() }),
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const toolFn = jasmine.createSpy("toolFn").and.returnValue(
|
|
171
|
+
Promise.resolve({
|
|
172
|
+
success: false,
|
|
173
|
+
error: "API unavailable",
|
|
174
|
+
code: "SERVICE_ERROR",
|
|
175
|
+
})
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const toolExecutor = new ToolExecutor(toolProps, toolFn as any, mockCtx);
|
|
179
|
+
const toolsMap = new Map([["get_weather", toolExecutor]]);
|
|
180
|
+
|
|
181
|
+
// Mock LLM adapter response with tool call
|
|
182
|
+
const errorMockAdapter = createStreamingMock([
|
|
183
|
+
// First call: agent requests tool
|
|
184
|
+
{
|
|
185
|
+
textContent: undefined,
|
|
186
|
+
toolCalls: [
|
|
187
|
+
{
|
|
188
|
+
id: "tool_1",
|
|
189
|
+
name: "get_weather",
|
|
190
|
+
input: { city: "Stockholm" },
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
usage: { inputTokens: 10, outputTokens: 20 },
|
|
194
|
+
stopReason: "tool_use" as const,
|
|
195
|
+
},
|
|
196
|
+
// Second call: agent handles error
|
|
197
|
+
{
|
|
198
|
+
textContent: "Sorry, weather service is unavailable",
|
|
199
|
+
toolCalls: [],
|
|
200
|
+
usage: { inputTokens: 15, outputTokens: 10 },
|
|
201
|
+
stopReason: "end_turn" as const,
|
|
202
|
+
}
|
|
203
|
+
]);
|
|
204
|
+
|
|
205
|
+
const agentProps: FlinkAgentProps = {
|
|
206
|
+
id: "weather_agent",
|
|
207
|
+
description: "Weather assistant",
|
|
208
|
+
instructions: "You help with weather",
|
|
209
|
+
tools: ["get_weather"],
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const llmAdapters = new Map();
|
|
213
|
+
llmAdapters.set("default", errorMockAdapter);
|
|
214
|
+
const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
|
|
215
|
+
|
|
216
|
+
const generator = runner.streamGenerator({ message: "What's the weather?" });
|
|
217
|
+
|
|
218
|
+
for await (const chunk of generator) {
|
|
219
|
+
if (chunk.type === "complete") {
|
|
220
|
+
expect(chunk.result.toolCalls.length).toBe(1);
|
|
221
|
+
expect(chunk.result.toolCalls[0].error).toBe("API unavailable");
|
|
222
|
+
expect(chunk.result.toolCalls[0].output).toBeNull();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("should handle missing tools", async () => {
|
|
228
|
+
const toolsMap = new Map(); // No tools registered
|
|
229
|
+
|
|
230
|
+
// Mock LLM adapter response requesting non-existent tool
|
|
231
|
+
const missingToolMockAdapter = createStreamingMock([
|
|
232
|
+
{
|
|
233
|
+
textContent: undefined,
|
|
234
|
+
toolCalls: [
|
|
235
|
+
{
|
|
236
|
+
id: "tool_1",
|
|
237
|
+
name: "missing_tool",
|
|
238
|
+
input: {},
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
usage: { inputTokens: 10, outputTokens: 20 },
|
|
242
|
+
stopReason: "tool_use" as const,
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
textContent: "Tool not available",
|
|
246
|
+
toolCalls: [],
|
|
247
|
+
usage: { inputTokens: 15, outputTokens: 10 },
|
|
248
|
+
stopReason: "end_turn" as const,
|
|
249
|
+
}
|
|
250
|
+
]);
|
|
251
|
+
|
|
252
|
+
const agentProps: FlinkAgentProps = {
|
|
253
|
+
id: "test_agent",
|
|
254
|
+
description: "Test agent",
|
|
255
|
+
instructions: "You are helpful",
|
|
256
|
+
tools: [],
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const llmAdapters = new Map();
|
|
260
|
+
llmAdapters.set("default", missingToolMockAdapter);
|
|
261
|
+
const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
|
|
262
|
+
|
|
263
|
+
const generator = runner.streamGenerator({ message: "Test" });
|
|
264
|
+
|
|
265
|
+
for await (const chunk of generator) {
|
|
266
|
+
if (chunk.type === "complete") {
|
|
267
|
+
expect(chunk.result.toolCalls.length).toBe(1);
|
|
268
|
+
expect(chunk.result.toolCalls[0].error).toContain("not found");
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe("Step limits", () => {
|
|
275
|
+
it("should respect maxSteps limit", async () => {
|
|
276
|
+
// Mock infinite tool calling loop
|
|
277
|
+
const infiniteLoopMockAdapter = createStreamingMock([{
|
|
278
|
+
textContent: undefined,
|
|
279
|
+
toolCalls: [
|
|
280
|
+
{
|
|
281
|
+
id: "tool_1",
|
|
282
|
+
name: "test_tool",
|
|
283
|
+
input: {},
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
usage: { inputTokens: 10, outputTokens: 20 },
|
|
287
|
+
stopReason: "tool_use" as const,
|
|
288
|
+
}]);
|
|
289
|
+
|
|
290
|
+
const toolProps: FlinkToolProps = {
|
|
291
|
+
id: "test_tool",
|
|
292
|
+
description: "Test tool",
|
|
293
|
+
inputSchema: z.object({}),
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const toolFn = async () => ({ success: true as const, data: {} });
|
|
297
|
+
const toolExecutor = new ToolExecutor(toolProps, toolFn, mockCtx);
|
|
298
|
+
const toolsMap = new Map([["test_tool", toolExecutor]]);
|
|
299
|
+
|
|
300
|
+
const agentProps: FlinkAgentProps = {
|
|
301
|
+
id: "test_agent",
|
|
302
|
+
description: "Test agent",
|
|
303
|
+
instructions: "You are helpful",
|
|
304
|
+
tools: ["test_tool"],
|
|
305
|
+
limits: { maxSteps: 3 },
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const llmAdapters = new Map();
|
|
309
|
+
llmAdapters.set("default", infiniteLoopMockAdapter);
|
|
310
|
+
const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
|
|
311
|
+
|
|
312
|
+
const generator = runner.streamGenerator({ message: "Test" });
|
|
313
|
+
|
|
314
|
+
for await (const chunk of generator) {
|
|
315
|
+
if (chunk.type === "complete") {
|
|
316
|
+
expect(chunk.result.stepsUsed).toBe(3);
|
|
317
|
+
expect(chunk.result.stoppedEarly).toBe(true);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("should allow runtime maxSteps override", async () => {
|
|
323
|
+
const runtimeOverrideMockAdapter = createStreamingMock([{
|
|
324
|
+
textContent: undefined,
|
|
325
|
+
toolCalls: [
|
|
326
|
+
{
|
|
327
|
+
id: "tool_1",
|
|
328
|
+
name: "test_tool",
|
|
329
|
+
input: {},
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
usage: { inputTokens: 10, outputTokens: 20 },
|
|
333
|
+
stopReason: "tool_use" as const,
|
|
334
|
+
}]);
|
|
335
|
+
|
|
336
|
+
const toolProps: FlinkToolProps = {
|
|
337
|
+
id: "test_tool",
|
|
338
|
+
description: "Test tool",
|
|
339
|
+
inputSchema: z.object({}),
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const toolFn = async () => ({ success: true as const, data: {} });
|
|
343
|
+
const toolExecutor = new ToolExecutor(toolProps, toolFn, mockCtx);
|
|
344
|
+
const toolsMap = new Map([["test_tool", toolExecutor]]);
|
|
345
|
+
|
|
346
|
+
const agentProps: FlinkAgentProps = {
|
|
347
|
+
id: "test_agent",
|
|
348
|
+
description: "Test agent",
|
|
349
|
+
instructions: "You are helpful",
|
|
350
|
+
tools: ["test_tool"],
|
|
351
|
+
limits: { maxSteps: 10 },
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const llmAdapters = new Map();
|
|
355
|
+
llmAdapters.set("default", runtimeOverrideMockAdapter);
|
|
356
|
+
const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
|
|
357
|
+
|
|
358
|
+
const generator = runner.streamGenerator({
|
|
359
|
+
message: "Test",
|
|
360
|
+
options: { maxSteps: 2 },
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
for await (const chunk of generator) {
|
|
364
|
+
if (chunk.type === "complete") {
|
|
365
|
+
expect(chunk.result.stepsUsed).toBe(2);
|
|
366
|
+
expect(chunk.result.stoppedEarly).toBe(true);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
describe("Message format conversion", () => {
|
|
373
|
+
it("should convert string to message array", async () => {
|
|
374
|
+
const agentProps: FlinkAgentProps = {
|
|
375
|
+
id: "test_agent",
|
|
376
|
+
description: "Test agent",
|
|
377
|
+
instructions: "You are helpful",
|
|
378
|
+
tools: [],
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const toolsMap = new Map();
|
|
382
|
+
const llmAdapters = new Map();
|
|
383
|
+
llmAdapters.set("default", mockLLMAdapter);
|
|
384
|
+
const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
|
|
385
|
+
|
|
386
|
+
const generator = runner.streamGenerator({ message: "Hello" });
|
|
387
|
+
|
|
388
|
+
for await (const chunk of generator) {
|
|
389
|
+
if (chunk.type === "complete") {
|
|
390
|
+
expect(chunk.result).toBeDefined();
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Verify LLM adapter was called with user message
|
|
395
|
+
expect(mockLLMAdapter.stream).toHaveBeenCalled();
|
|
396
|
+
const callArgs = (mockLLMAdapter.stream as jasmine.Spy).calls.first().args[0];
|
|
397
|
+
expect(callArgs.messages.length).toBeGreaterThanOrEqual(1);
|
|
398
|
+
expect(callArgs.messages[0]).toEqual({ role: "user", content: "Hello" });
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it("should convert Message[] to Anthropic format", async () => {
|
|
402
|
+
const agentProps: FlinkAgentProps = {
|
|
403
|
+
id: "test_agent",
|
|
404
|
+
description: "Test agent",
|
|
405
|
+
instructions: "You are helpful",
|
|
406
|
+
tools: [],
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const toolsMap = new Map();
|
|
410
|
+
const llmAdapters = new Map();
|
|
411
|
+
llmAdapters.set("default", mockLLMAdapter);
|
|
412
|
+
const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
|
|
413
|
+
|
|
414
|
+
const generator = runner.streamGenerator({
|
|
415
|
+
message: [
|
|
416
|
+
{ role: "user", content: "Hello" },
|
|
417
|
+
{ role: "user", content: "How are you?" },
|
|
418
|
+
],
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
for await (const chunk of generator) {
|
|
422
|
+
if (chunk.type === "complete") {
|
|
423
|
+
expect(chunk.result).toBeDefined();
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
expect(mockLLMAdapter.stream).toHaveBeenCalled();
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
describe("Model configuration", () => {
|
|
432
|
+
it("should pass configuration to LLM adapter", async () => {
|
|
433
|
+
const agentProps: FlinkAgentProps = {
|
|
434
|
+
id: "test_agent",
|
|
435
|
+
description: "Test agent",
|
|
436
|
+
instructions: "You are helpful",
|
|
437
|
+
tools: [],
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const toolsMap = new Map();
|
|
441
|
+
const llmAdapters = new Map();
|
|
442
|
+
llmAdapters.set("default", mockLLMAdapter);
|
|
443
|
+
const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
|
|
444
|
+
|
|
445
|
+
const generator = runner.streamGenerator({ message: "Hello" });
|
|
446
|
+
|
|
447
|
+
for await (const chunk of generator) {
|
|
448
|
+
if (chunk.type === "complete") {
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Verify LLM adapter was called via stream
|
|
454
|
+
expect(mockLLMAdapter.stream).toHaveBeenCalled();
|
|
455
|
+
const callArgs = (mockLLMAdapter.stream as jasmine.Spy).calls.mostRecent().args[0];
|
|
456
|
+
expect(callArgs.instructions).toBe("You are helpful");
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it("should pass custom temperature and maxTokens", async () => {
|
|
460
|
+
const agentProps: FlinkAgentProps = {
|
|
461
|
+
id: "test_agent",
|
|
462
|
+
description: "Test agent",
|
|
463
|
+
instructions: "You are helpful",
|
|
464
|
+
tools: [],
|
|
465
|
+
model: {
|
|
466
|
+
temperature: 0.3,
|
|
467
|
+
maxTokens: 2000,
|
|
468
|
+
},
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
const toolsMap = new Map();
|
|
472
|
+
const llmAdapters = new Map();
|
|
473
|
+
llmAdapters.set("default", mockLLMAdapter);
|
|
474
|
+
const runner = new AgentRunner(agentProps, toolsMap, llmAdapters);
|
|
475
|
+
|
|
476
|
+
const generator = runner.streamGenerator({ message: "Hello" });
|
|
477
|
+
|
|
478
|
+
for await (const chunk of generator) {
|
|
479
|
+
if (chunk.type === "complete") {
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const callArgs = (mockLLMAdapter.stream as jasmine.Spy).calls.mostRecent().args[0];
|
|
485
|
+
expect(callArgs.temperature).toBe(0.3);
|
|
486
|
+
expect(callArgs.maxTokens).toBe(2000);
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
describe("Error handling", () => {
|
|
491
|
+
it("should throw error when LLM adapter not configured", () => {
|
|
492
|
+
const agentProps: FlinkAgentProps = {
|
|
493
|
+
id: "test_agent",
|
|
494
|
+
description: "Test agent",
|
|
495
|
+
instructions: "You are helpful",
|
|
496
|
+
tools: [],
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
const toolsMap = new Map();
|
|
500
|
+
const llmAdapters = new Map(); // Empty map
|
|
501
|
+
|
|
502
|
+
expect(() => {
|
|
503
|
+
new AgentRunner(agentProps, toolsMap, llmAdapters);
|
|
504
|
+
}).toThrowError(/not configured/);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it("should throw error for unregistered adapter", () => {
|
|
508
|
+
const agentProps: FlinkAgentProps = {
|
|
509
|
+
id: "test_agent",
|
|
510
|
+
description: "Test agent",
|
|
511
|
+
instructions: "You are helpful",
|
|
512
|
+
tools: [],
|
|
513
|
+
model: {
|
|
514
|
+
adapterId: "openai",
|
|
515
|
+
},
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
const toolsMap = new Map();
|
|
519
|
+
const llmAdapters = new Map();
|
|
520
|
+
llmAdapters.set("default", mockLLMAdapter);
|
|
521
|
+
|
|
522
|
+
expect(() => {
|
|
523
|
+
new AgentRunner(agentProps, toolsMap, llmAdapters);
|
|
524
|
+
}).toThrowError(/LLM adapter "openai" not configured/);
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
});
|