@flink-app/flink 0.14.3 → 2.0.0-alpha.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/cli/build.ts +8 -1
  3. package/cli/run.ts +8 -1
  4. package/dist/cli/build.js +8 -1
  5. package/dist/cli/run.js +8 -1
  6. package/dist/src/FlinkApp.d.ts +33 -0
  7. package/dist/src/FlinkApp.js +279 -35
  8. package/dist/src/FlinkContext.d.ts +21 -0
  9. package/dist/src/FlinkHttpHandler.d.ts +152 -9
  10. package/dist/src/FlinkHttpHandler.js +37 -1
  11. package/dist/src/TypeScriptCompiler.d.ts +42 -0
  12. package/dist/src/TypeScriptCompiler.js +346 -4
  13. package/dist/src/TypeScriptUtils.js +4 -0
  14. package/dist/src/ai/AgentRunner.d.ts +39 -0
  15. package/dist/src/ai/AgentRunner.js +625 -0
  16. package/dist/src/ai/FlinkAgent.d.ts +446 -0
  17. package/dist/src/ai/FlinkAgent.js +633 -0
  18. package/dist/src/ai/FlinkTool.d.ts +37 -0
  19. package/dist/src/ai/FlinkTool.js +2 -0
  20. package/dist/src/ai/LLMAdapter.d.ts +119 -0
  21. package/dist/src/ai/LLMAdapter.js +2 -0
  22. package/dist/src/ai/SubAgentExecutor.d.ts +36 -0
  23. package/dist/src/ai/SubAgentExecutor.js +220 -0
  24. package/dist/src/ai/ToolExecutor.d.ts +35 -0
  25. package/dist/src/ai/ToolExecutor.js +237 -0
  26. package/dist/src/ai/index.d.ts +5 -0
  27. package/dist/src/ai/index.js +21 -0
  28. package/dist/src/handlers/StreamWriterFactory.d.ts +20 -0
  29. package/dist/src/handlers/StreamWriterFactory.js +83 -0
  30. package/dist/src/index.d.ts +4 -0
  31. package/dist/src/index.js +4 -0
  32. package/dist/src/utils.d.ts +30 -0
  33. package/dist/src/utils.js +52 -0
  34. package/package.json +16 -2
  35. package/readme.md +425 -0
  36. package/spec/AgentDuplicateDetection.spec.ts +112 -0
  37. package/spec/AgentRunner.spec.ts +527 -0
  38. package/spec/ConversationHooks.spec.ts +290 -0
  39. package/spec/FlinkAgent.spec.ts +310 -0
  40. package/spec/FlinkApp.onError.spec.ts +1 -2
  41. package/spec/FlinkApp.query.spec.ts +107 -0
  42. package/spec/FlinkApp.validationMode.spec.ts +155 -0
  43. package/spec/StreamingIntegration.spec.ts +138 -0
  44. package/spec/SubAgentSupport.spec.ts +941 -0
  45. package/spec/ToolExecutor.spec.ts +360 -0
  46. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar.js +57 -0
  47. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar2.js +59 -0
  48. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema.js +53 -0
  49. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema2.js +53 -0
  50. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema3.js +53 -0
  51. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema.js +55 -0
  52. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema2.js +55 -0
  53. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile.js +58 -0
  54. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile2.js +58 -0
  55. package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler.js +53 -0
  56. package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler2.js +55 -0
  57. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchCar.js +58 -0
  58. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOnboardingSession.js +76 -0
  59. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOrderWithComplexTypes.js +58 -0
  60. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchProductWithIntersection.js +59 -0
  61. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchUserWithUnion.js +59 -0
  62. package/spec/mock-project/dist/spec/mock-project/src/handlers/PostCar.js +55 -0
  63. package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogin.js +56 -0
  64. package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogout.js +55 -0
  65. package/spec/mock-project/dist/spec/mock-project/src/handlers/PutCar.js +55 -0
  66. package/spec/mock-project/dist/spec/mock-project/src/index.js +83 -0
  67. package/spec/mock-project/dist/spec/mock-project/src/repos/CarRepo.js +26 -0
  68. package/spec/mock-project/dist/spec/mock-project/src/schemas/Car.js +2 -0
  69. package/spec/mock-project/dist/spec/mock-project/src/schemas/DefaultExportSchema.js +2 -0
  70. package/spec/mock-project/dist/spec/mock-project/src/schemas/FileWithTwoSchemas.js +2 -0
  71. package/spec/mock-project/dist/src/FlinkApp.js +1012 -0
  72. package/spec/mock-project/dist/src/FlinkContext.js +2 -0
  73. package/spec/mock-project/dist/src/FlinkErrors.js +143 -0
  74. package/spec/mock-project/dist/src/FlinkHttpHandler.js +47 -0
  75. package/spec/mock-project/dist/src/FlinkJob.js +2 -0
  76. package/spec/mock-project/dist/src/FlinkLog.js +26 -0
  77. package/spec/mock-project/dist/src/FlinkPlugin.js +2 -0
  78. package/spec/mock-project/dist/src/FlinkRepo.js +224 -0
  79. package/spec/mock-project/dist/src/FlinkResponse.js +2 -0
  80. package/spec/mock-project/dist/src/ai/AgentExecutor.js +279 -0
  81. package/spec/mock-project/dist/src/ai/AgentRunner.js +625 -0
  82. package/spec/mock-project/dist/src/ai/FlinkAgent.js +633 -0
  83. package/spec/mock-project/dist/src/ai/FlinkTool.js +2 -0
  84. package/spec/mock-project/dist/src/ai/LLMAdapter.js +2 -0
  85. package/spec/mock-project/dist/src/ai/SubAgentExecutor.js +220 -0
  86. package/spec/mock-project/dist/src/ai/ToolExecutor.js +237 -0
  87. package/spec/mock-project/dist/src/auth/FlinkAuthPlugin.js +2 -0
  88. package/spec/mock-project/dist/src/auth/FlinkAuthUser.js +2 -0
  89. package/spec/mock-project/dist/src/handlers/StreamWriterFactory.js +83 -0
  90. package/spec/mock-project/dist/src/index.js +17 -69
  91. package/spec/mock-project/dist/src/mock-data-generator.js +9 -0
  92. package/spec/mock-project/dist/src/utils.js +290 -0
  93. package/spec/mock-project/tsconfig.json +6 -1
  94. package/spec/testHelpers.ts +49 -0
  95. package/spec/utils.caseConversion.spec.ts +80 -0
  96. package/spec/utils.spec.ts +13 -13
  97. package/src/FlinkApp.ts +275 -8
  98. package/src/FlinkContext.ts +22 -0
  99. package/src/FlinkHttpHandler.ts +164 -10
  100. package/src/TypeScriptCompiler.ts +398 -7
  101. package/src/TypeScriptUtils.ts +5 -0
  102. package/src/ai/AgentRunner.ts +549 -0
  103. package/src/ai/FlinkAgent.ts +770 -0
  104. package/src/ai/FlinkTool.ts +40 -0
  105. package/src/ai/LLMAdapter.ts +96 -0
  106. package/src/ai/SubAgentExecutor.ts +199 -0
  107. package/src/ai/ToolExecutor.ts +193 -0
  108. package/src/ai/index.ts +5 -0
  109. package/src/handlers/StreamWriterFactory.ts +84 -0
  110. package/src/index.ts +4 -0
  111. package/src/utils.ts +52 -0
  112. package/tsconfig.json +6 -1
@@ -0,0 +1,290 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __generator = (this && this.__generator) || function (thisArg, body) {
12
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
13
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14
+ function verb(n) { return function (v) { return step([n, v]); }; }
15
+ function step(op) {
16
+ if (f) throw new TypeError("Generator is already executing.");
17
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
18
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19
+ if (y = 0, t) op = [op[0] & 2, t.value];
20
+ switch (op[0]) {
21
+ case 0: case 1: t = op; break;
22
+ case 4: _.label++; return { value: op[1], done: false };
23
+ case 5: _.label++; y = op[1]; op = [0]; continue;
24
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
25
+ default:
26
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30
+ if (t[2]) _.ops.pop();
31
+ _.trys.pop(); continue;
32
+ }
33
+ op = body.call(thisArg, _);
34
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
+ }
37
+ };
38
+ var __importDefault = (this && this.__importDefault) || function (mod) {
39
+ return (mod && mod.__esModule) ? mod : { "default": mod };
40
+ };
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.handlersPath = handlersPath;
43
+ exports.schemasPath = schemasPath;
44
+ exports.isRouteMatch = isRouteMatch;
45
+ exports.isError = isError;
46
+ exports.getHandlerFiles = getHandlerFiles;
47
+ exports.getSchemaFiles = getSchemaFiles;
48
+ exports.getCollectionNameForRepo = getCollectionNameForRepo;
49
+ exports.getRepoInstanceName = getRepoInstanceName;
50
+ exports.toKebabCase = toKebabCase;
51
+ exports.toSnakeCase = toSnakeCase;
52
+ exports.getHttpMethodFromHandlerName = getHttpMethodFromHandlerName;
53
+ exports.getJsDocComment = getJsDocComment;
54
+ exports.getPathParams = getPathParams;
55
+ exports.getDataAtPath = getDataAtPath;
56
+ exports.formatValidationErrors = formatValidationErrors;
57
+ exports.generateShortId = generateShortId;
58
+ var path_1 = require("path");
59
+ var tiny_glob_1 = __importDefault(require("tiny-glob"));
60
+ var FlinkHttpHandler_1 = require("./FlinkHttpHandler");
61
+ var FlinkLog_1 = require("./FlinkLog");
62
+ function handlersPath(appRoot) {
63
+ return (0, path_1.join)(appRoot, "src", "handlers");
64
+ }
65
+ function schemasPath(appRoot) {
66
+ return (0, path_1.join)(appRoot, "src", "schemas");
67
+ }
68
+ function isRouteMatch(req, routes) {
69
+ var match = routes.find(function (_a) {
70
+ var method = _a.method, path = _a.path;
71
+ var sameMethod = req.method.toLowerCase() === method;
72
+ var samePath = req.route.path === path;
73
+ return sameMethod && samePath;
74
+ });
75
+ return !!match;
76
+ }
77
+ function isError(message) {
78
+ return message.status && message.status > 399;
79
+ }
80
+ function getHandlerFiles(appRoot) {
81
+ return __awaiter(this, void 0, void 0, function () {
82
+ var err_1;
83
+ return __generator(this, function (_a) {
84
+ switch (_a.label) {
85
+ case 0:
86
+ _a.trys.push([0, 2, , 3]);
87
+ return [4 /*yield*/, (0, tiny_glob_1.default)("**/*.ts", {
88
+ cwd: handlersPath(appRoot),
89
+ absolute: true,
90
+ })];
91
+ case 1: return [2 /*return*/, _a.sent()];
92
+ case 2:
93
+ err_1 = _a.sent();
94
+ FlinkLog_1.log.debug("Failed getting handler files: ".concat(err_1));
95
+ return [2 /*return*/, []];
96
+ case 3: return [2 /*return*/];
97
+ }
98
+ });
99
+ });
100
+ }
101
+ function getSchemaFiles(appRoot) {
102
+ return __awaiter(this, void 0, void 0, function () {
103
+ var err_2;
104
+ return __generator(this, function (_a) {
105
+ switch (_a.label) {
106
+ case 0:
107
+ _a.trys.push([0, 2, , 3]);
108
+ return [4 /*yield*/, (0, tiny_glob_1.default)("**/*.ts", {
109
+ cwd: schemasPath(appRoot),
110
+ absolute: true,
111
+ })];
112
+ case 1: return [2 /*return*/, _a.sent()];
113
+ case 2:
114
+ err_2 = _a.sent();
115
+ return [2 /*return*/, []];
116
+ case 3: return [2 /*return*/];
117
+ }
118
+ });
119
+ });
120
+ }
121
+ function getCollectionNameForRepo(repoFilename) {
122
+ return repoFilename.replace("Repo.ts", "").toLowerCase();
123
+ }
124
+ function getRepoInstanceName(fn) {
125
+ var name = fn.split(".ts")[0];
126
+ return name.charAt(0).toLowerCase() + name.substr(1);
127
+ }
128
+ /**
129
+ * Convert PascalCase or camelCase to kebab-case
130
+ *
131
+ * Examples:
132
+ * - FooBarBaz → foo-bar-baz
133
+ * - CarAgent → car-agent
134
+ * - APIAgent → api-agent
135
+ * - HTMLParser → html-parser
136
+ * - IOManager → io-manager
137
+ *
138
+ * Handles:
139
+ * - Single uppercase followed by lowercase: CarAgent → car-agent
140
+ * - Multiple consecutive uppercase: APIAgent → api-agent
141
+ * - All caps: HTML → html
142
+ */
143
+ function toKebabCase(str) {
144
+ return str
145
+ // Insert hyphen before uppercase letters that follow lowercase letters
146
+ // CarAgent → Car-Agent
147
+ .replace(/([a-z])([A-Z])/g, "$1-$2")
148
+ // Insert hyphen before uppercase letter that follows uppercase and precedes lowercase
149
+ // APIAgent → API-Agent
150
+ .replace(/([A-Z])([A-Z][a-z])/g, "$1-$2")
151
+ // Convert to lowercase
152
+ .toLowerCase();
153
+ }
154
+ /**
155
+ * Convert kebab-case to snake_case
156
+ *
157
+ * Examples:
158
+ * - car-agent → car_agent
159
+ * - api-agent → api_agent
160
+ * - html-parser → html_parser
161
+ */
162
+ function toSnakeCase(str) {
163
+ return str.replace(/-/g, "_");
164
+ }
165
+ /**
166
+ * Get http method from props or convention based on file name
167
+ * if it starts with i.e "GetFoo"
168
+ */
169
+ function getHttpMethodFromHandlerName(handlerFilename) {
170
+ if (handlerFilename.includes(path_1.sep)) {
171
+ var split = handlerFilename.split(path_1.sep);
172
+ handlerFilename = split[split.length - 1];
173
+ }
174
+ handlerFilename = handlerFilename.toLocaleLowerCase();
175
+ if (handlerFilename.startsWith(FlinkHttpHandler_1.HttpMethod.get))
176
+ return FlinkHttpHandler_1.HttpMethod.get;
177
+ if (handlerFilename.startsWith(FlinkHttpHandler_1.HttpMethod.post))
178
+ return FlinkHttpHandler_1.HttpMethod.post;
179
+ if (handlerFilename.startsWith(FlinkHttpHandler_1.HttpMethod.put))
180
+ return FlinkHttpHandler_1.HttpMethod.put;
181
+ if (handlerFilename.startsWith(FlinkHttpHandler_1.HttpMethod.delete))
182
+ return FlinkHttpHandler_1.HttpMethod.delete;
183
+ if (handlerFilename.startsWith(FlinkHttpHandler_1.HttpMethod.patch))
184
+ return FlinkHttpHandler_1.HttpMethod.patch;
185
+ }
186
+ function getJsDocComment(comment) {
187
+ var rows = comment.split("\n").map(function (line) {
188
+ line = line.trim();
189
+ if (line.startsWith("//")) {
190
+ return line.replace("//", line).trim();
191
+ }
192
+ return line.replace(/\/\*\*|\*\/|\*/, "").trim();
193
+ });
194
+ return rows.join("\n").trim();
195
+ }
196
+ var pathParamsRegex = /:([a-zA-Z0-9]+)/g;
197
+ /**
198
+ * Returns array of path params from path string.
199
+ * For example `/user/:id` will return `["id"]`
200
+ * @param path
201
+ * @returns
202
+ */
203
+ function getPathParams(path) {
204
+ var _a;
205
+ return ((_a = path.match(pathParamsRegex)) === null || _a === void 0 ? void 0 : _a.map(function (match) { return match.slice(1); })) || [];
206
+ }
207
+ /**
208
+ * Extracts data at a given JSON path (e.g., "/jobs/5/result")
209
+ * Returns the value at that path, or undefined if not found
210
+ */
211
+ function getDataAtPath(data, instancePath) {
212
+ if (!instancePath || instancePath === "/") {
213
+ return data;
214
+ }
215
+ var parts = instancePath.split("/").filter(function (p) { return p.length > 0; });
216
+ var current = data;
217
+ for (var _i = 0, parts_1 = parts; _i < parts_1.length; _i++) {
218
+ var part = parts_1[_i];
219
+ if (current === undefined || current === null) {
220
+ return undefined;
221
+ }
222
+ current = current[part];
223
+ }
224
+ return current;
225
+ }
226
+ /**
227
+ * Formats validation errors with context about the problematic data
228
+ * @param errors AJV validation errors
229
+ * @param data The full data object that failed validation
230
+ * @param maxDataLength Maximum length of data to show (default 500)
231
+ */
232
+ function formatValidationErrors(errors, data, maxDataLength) {
233
+ if (maxDataLength === void 0) { maxDataLength = 500; }
234
+ if (!errors || errors.length === 0) {
235
+ return "Unknown validation error";
236
+ }
237
+ var formatted = [];
238
+ // Group errors by instance path to avoid repetition
239
+ var errorsByPath = new Map();
240
+ for (var _i = 0, errors_1 = errors; _i < errors_1.length; _i++) {
241
+ var error = errors_1[_i];
242
+ var path = error.instancePath || "/";
243
+ if (!errorsByPath.has(path)) {
244
+ errorsByPath.set(path, []);
245
+ }
246
+ errorsByPath.get(path).push(error);
247
+ }
248
+ errorsByPath.forEach(function (pathErrors, path) {
249
+ var dataAtPath = getDataAtPath(data, path);
250
+ var dataStr = JSON.stringify(dataAtPath);
251
+ // Truncate if too long
252
+ if (dataStr.length > maxDataLength) {
253
+ dataStr = dataStr.substring(0, maxDataLength) + "... (truncated)";
254
+ }
255
+ formatted.push("\nPath: ".concat(path));
256
+ formatted.push("Data: ".concat(dataStr));
257
+ formatted.push("Errors:");
258
+ for (var _i = 0, pathErrors_1 = pathErrors; _i < pathErrors_1.length; _i++) {
259
+ var error = pathErrors_1[_i];
260
+ if (error.keyword === "required") {
261
+ formatted.push(" - Missing required property: ".concat(error.params.missingProperty));
262
+ }
263
+ else if (error.keyword === "type") {
264
+ formatted.push(" - Invalid type at ".concat(error.schemaPath, ": expected ").concat(error.params.type, ", got ").concat(typeof dataAtPath));
265
+ }
266
+ else if (error.keyword === "additionalProperties") {
267
+ formatted.push(" - Additional property not allowed: ".concat(error.params.additionalProperty));
268
+ }
269
+ else if (error.keyword === "anyOf" || error.keyword === "oneOf") {
270
+ formatted.push(" - ".concat(error.message));
271
+ }
272
+ else {
273
+ formatted.push(" - ".concat(error.message, " (").concat(error.keyword, ")"));
274
+ }
275
+ }
276
+ });
277
+ return formatted.join("\n");
278
+ }
279
+ /**
280
+ * Generate a short random ID for nested conversations
281
+ * Format: 8 character alphanumeric string (e.g., "a3b7c9d2")
282
+ */
283
+ function generateShortId() {
284
+ var chars = "abcdefghijklmnopqrstuvwxyz0123456789";
285
+ var result = "";
286
+ for (var i = 0; i < 8; i++) {
287
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
288
+ }
289
+ return result;
290
+ }
@@ -15,7 +15,12 @@
15
15
  "noEmit": true,
16
16
  "experimentalDecorators": true,
17
17
  "checkJs": false,
18
- "typeRoots": ["node_modules/@types", "node_modules/@flink-app"]
18
+ "typeRoots": ["node_modules/@types", "node_modules/@flink-app"],
19
+ "baseUrl": ".",
20
+ "paths": {
21
+ "@flink-app/flink": ["../../src/index.ts"],
22
+ "@flink-app/flink/*": ["../../src/*"]
23
+ }
19
24
  },
20
25
  "include": ["./src/**/*.ts", "./.flink/**/*.ts"],
21
26
  "exclude": ["./node_modules/*"]
@@ -0,0 +1,49 @@
1
+ import { LLMAdapter, LLMStreamChunk } from "../src/ai/LLMAdapter";
2
+
3
+ /**
4
+ * Mock response data for tests
5
+ */
6
+ export interface MockLLMResponse {
7
+ textContent?: string;
8
+ toolCalls: Array<{ id: string; name: string; input: any }>;
9
+ usage: { inputTokens: number; outputTokens: number };
10
+ stopReason: "end_turn" | "tool_use" | "max_tokens";
11
+ }
12
+
13
+ /**
14
+ * Helper to create a streaming mock LLM adapter from responses.
15
+ * LLM adapters are streaming-only, so this creates a mock stream() implementation.
16
+ *
17
+ * @param responses Array of responses to return on subsequent calls
18
+ * @returns LLMAdapter with stream method implemented
19
+ */
20
+ export function createStreamingMock(responses: MockLLMResponse[]): LLMAdapter {
21
+ let callIndex = 0;
22
+
23
+ const streamGenerator = async function* () {
24
+ const response = responses[callIndex] || responses[responses.length - 1];
25
+ callIndex++;
26
+
27
+ // Yield text content if present
28
+ if (response.textContent) {
29
+ yield { type: "text", delta: response.textContent } as LLMStreamChunk;
30
+ }
31
+
32
+ // Yield tool calls if present
33
+ if (response.toolCalls && response.toolCalls.length > 0) {
34
+ for (const toolCall of response.toolCalls) {
35
+ yield { type: "tool_call", toolCall } as LLMStreamChunk;
36
+ }
37
+ }
38
+
39
+ // Yield usage information
40
+ yield { type: "usage", usage: response.usage } as LLMStreamChunk;
41
+
42
+ // Yield done event
43
+ yield { type: "done", stopReason: response.stopReason } as LLMStreamChunk;
44
+ };
45
+
46
+ return {
47
+ stream: jasmine.createSpy("stream").and.callFake(streamGenerator),
48
+ };
49
+ }
@@ -0,0 +1,80 @@
1
+ import { toKebabCase, toSnakeCase } from "../src/utils";
2
+
3
+ describe("Case Conversion Utilities", () => {
4
+ describe("toKebabCase", () => {
5
+ it("should handle standard PascalCase", () => {
6
+ expect(toKebabCase("CarAgent")).toBe("car-agent");
7
+ expect(toKebabCase("FooBarBaz")).toBe("foo-bar-baz");
8
+ expect(toKebabCase("CarPricingAgent")).toBe("car-pricing-agent");
9
+ });
10
+
11
+ it("should handle consecutive capitals (acronyms)", () => {
12
+ expect(toKebabCase("APIAgent")).toBe("api-agent");
13
+ expect(toKebabCase("HTMLParser")).toBe("html-parser");
14
+ expect(toKebabCase("XMLHttpRequest")).toBe("xml-http-request");
15
+ expect(toKebabCase("IOManager")).toBe("io-manager");
16
+ });
17
+
18
+ it("should handle camelCase with acronyms", () => {
19
+ expect(toKebabCase("getHTMLContent")).toBe("get-html-content");
20
+ expect(toKebabCase("parseJSONData")).toBe("parse-json-data");
21
+ });
22
+
23
+ it("should handle single and double letter strings", () => {
24
+ expect(toKebabCase("A")).toBe("a");
25
+ expect(toKebabCase("AB")).toBe("ab");
26
+ expect(toKebabCase("ABC")).toBe("abc");
27
+ });
28
+
29
+ it("should handle already lowercase strings", () => {
30
+ expect(toKebabCase("a")).toBe("a");
31
+ expect(toKebabCase("abc")).toBe("abc");
32
+ });
33
+
34
+ it("should handle camelCase starting with lowercase", () => {
35
+ expect(toKebabCase("aB")).toBe("a-b");
36
+ expect(toKebabCase("carAgent")).toBe("car-agent");
37
+ });
38
+
39
+ it("should handle real-world agent class names", () => {
40
+ expect(toKebabCase("OrchestratorAgent")).toBe("orchestrator-agent");
41
+ expect(toKebabCase("CarRecommendationAgent")).toBe("car-recommendation-agent");
42
+ expect(toKebabCase("UserAuthenticationAgent")).toBe("user-authentication-agent");
43
+ });
44
+ });
45
+
46
+ describe("toSnakeCase", () => {
47
+ it("should convert kebab-case to snake_case", () => {
48
+ expect(toSnakeCase("car-agent")).toBe("car_agent");
49
+ expect(toSnakeCase("foo-bar-baz")).toBe("foo_bar_baz");
50
+ expect(toSnakeCase("api-agent")).toBe("api_agent");
51
+ });
52
+
53
+ it("should handle single words", () => {
54
+ expect(toSnakeCase("car")).toBe("car");
55
+ expect(toSnakeCase("a")).toBe("a");
56
+ });
57
+
58
+ it("should handle multiple hyphens", () => {
59
+ expect(toSnakeCase("xml-http-request")).toBe("xml_http_request");
60
+ expect(toSnakeCase("car-pricing-agent")).toBe("car_pricing_agent");
61
+ });
62
+
63
+ it("should convert agent IDs to tool names correctly", () => {
64
+ // Simulating the full conversion pipeline
65
+ const agentClass = "CarAgent";
66
+ const kebab = toKebabCase(agentClass);
67
+ const toolName = `ask_${toSnakeCase(kebab)}`;
68
+
69
+ expect(toolName).toBe("ask_car_agent");
70
+ });
71
+
72
+ it("should handle acronym-based agent names", () => {
73
+ const agentClass = "APIAgent";
74
+ const kebab = toKebabCase(agentClass);
75
+ const toolName = `ask_${toSnakeCase(kebab)}`;
76
+
77
+ expect(toolName).toBe("ask_api_agent");
78
+ });
79
+ });
80
+ });
@@ -48,9 +48,9 @@ describe("Utils", () => {
48
48
 
49
49
  const formatted = formatValidationErrors(errors, data);
50
50
 
51
- expect(formatted).toContain('Path: /name');
52
- expect(formatted).toContain('Data: 123');
53
- expect(formatted).toContain('expected string');
51
+ expect(formatted).toContain("Path: /name");
52
+ expect(formatted).toContain("Data: 123");
53
+ expect(formatted).toContain("expected string");
54
54
  });
55
55
 
56
56
  it("should format missing required property errors", () => {
@@ -67,9 +67,9 @@ describe("Utils", () => {
67
67
 
68
68
  const formatted = formatValidationErrors(errors, data);
69
69
 
70
- expect(formatted).toContain('Path: /user');
70
+ expect(formatted).toContain("Path: /user");
71
71
  expect(formatted).toContain('Data: {"name":"John"}');
72
- expect(formatted).toContain('Missing required property: email');
72
+ expect(formatted).toContain("Missing required property: email");
73
73
  });
74
74
 
75
75
  it("should format errors for array items", () => {
@@ -95,9 +95,9 @@ describe("Utils", () => {
95
95
 
96
96
  const formatted = formatValidationErrors(errors, data);
97
97
 
98
- expect(formatted).toContain('Path: /jobs/5/result');
98
+ expect(formatted).toContain("Path: /jobs/5/result");
99
99
  expect(formatted).toContain('Data: {"seed":"wrong-type"}');
100
- expect(formatted).toContain('must match a schema in anyOf');
100
+ expect(formatted).toContain("must match a schema in anyOf");
101
101
  });
102
102
 
103
103
  it("should truncate very long data", () => {
@@ -115,7 +115,7 @@ describe("Utils", () => {
115
115
 
116
116
  const formatted = formatValidationErrors(errors, data, 100);
117
117
 
118
- expect(formatted).toContain('(truncated)');
118
+ expect(formatted).toContain("(truncated)");
119
119
  expect(formatted.length).toBeLessThan(500);
120
120
  });
121
121
 
@@ -145,8 +145,8 @@ describe("Utils", () => {
145
145
  expect(pathCount).toBe(1);
146
146
 
147
147
  // But should list both missing properties
148
- expect(formatted).toContain('Missing required property: email');
149
- expect(formatted).toContain('Missing required property: age');
148
+ expect(formatted).toContain("Missing required property: email");
149
+ expect(formatted).toContain("Missing required property: age");
150
150
  });
151
151
 
152
152
  it("should show specific additional property names", () => {
@@ -171,9 +171,9 @@ describe("Utils", () => {
171
171
  const formatted = formatValidationErrors(errors, data);
172
172
 
173
173
  // This formatted output is used in HTTP error responses (400/500) in the error.detail field
174
- expect(formatted).toContain('Path: /');
175
- expect(formatted).toContain('Additional property not allowed: extraField1');
176
- expect(formatted).toContain('Additional property not allowed: extraField2');
174
+ expect(formatted).toContain("Path: /");
175
+ expect(formatted).toContain("Additional property not allowed: extraField1");
176
+ expect(formatted).toContain("Additional property not allowed: extraField2");
177
177
  });
178
178
  });
179
179
  });