@flink-app/flink 2.0.0-alpha.61 → 2.0.0-alpha.63
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 +35 -0
- package/dist/src/FlinkApp.js +67 -30
- package/dist/src/FlinkRepo.d.ts +1 -1
- package/dist/src/ai/FlinkAgent.d.ts +45 -1
- package/dist/src/ai/FlinkAgent.js +24 -6
- package/dist/src/ai/ToolExecutor.d.ts +3 -1
- package/dist/src/ai/ToolExecutor.js +46 -60
- package/dist/src/ai/agentInstructions.d.ts +1 -1
- package/dist/src/ai/instructionFileLoader.d.ts +44 -0
- package/dist/src/ai/instructionFileLoader.js +151 -0
- package/package.json +1 -1
- package/spec/AgentDescendantDetection.spec.ts +2 -2
- package/spec/AgentRunner.spec.ts +1 -1
- package/spec/ConversationHooks.spec.ts +5 -5
- package/spec/FlinkAgent.spec.ts +16 -16
- package/spec/FlinkApp.undefinedResponse.spec.ts +123 -0
- package/spec/FlinkJob.spec.ts +95 -0
- package/spec/StreamingIntegration.spec.ts +1 -1
- package/spec/ai/ContextCompaction.spec.ts +1 -1
- package/spec/ai/ConversationAgent.spec.ts +1 -1
- package/spec/ai/InMemoryConversationAgent.spec.ts +1 -1
- package/spec/fixtures/agent-instructions/TestAgent.ts +11 -9
- package/src/FlinkApp.ts +55 -27
- package/src/FlinkRepo.ts +1 -1
- package/src/ai/FlinkAgent.ts +56 -5
- package/src/ai/ToolExecutor.ts +39 -50
- package/src/ai/agentInstructions.ts +1 -1
- package/src/ai/instructionFileLoader.ts +126 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __assign = (this && this.__assign) || function () {
|
|
3
|
+
__assign = Object.assign || function(t) {
|
|
4
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
5
|
+
s = arguments[i];
|
|
6
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
7
|
+
t[p] = s[p];
|
|
8
|
+
}
|
|
9
|
+
return t;
|
|
10
|
+
};
|
|
11
|
+
return __assign.apply(this, arguments);
|
|
12
|
+
};
|
|
13
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
16
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
17
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
18
|
+
}
|
|
19
|
+
Object.defineProperty(o, k2, desc);
|
|
20
|
+
}) : (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
o[k2] = m[k];
|
|
23
|
+
}));
|
|
24
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
25
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
26
|
+
}) : function(o, v) {
|
|
27
|
+
o["default"] = v;
|
|
28
|
+
});
|
|
29
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
30
|
+
if (mod && mod.__esModule) return mod;
|
|
31
|
+
var result = {};
|
|
32
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
33
|
+
__setModuleDefault(result, mod);
|
|
34
|
+
return result;
|
|
35
|
+
};
|
|
36
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
37
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
38
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
39
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
40
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
41
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
42
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
46
|
+
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);
|
|
47
|
+
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
48
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
49
|
+
function step(op) {
|
|
50
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
51
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
52
|
+
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;
|
|
53
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
54
|
+
switch (op[0]) {
|
|
55
|
+
case 0: case 1: t = op; break;
|
|
56
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
57
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
58
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
59
|
+
default:
|
|
60
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
61
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
62
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
63
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
64
|
+
if (t[2]) _.ops.pop();
|
|
65
|
+
_.trys.pop(); continue;
|
|
66
|
+
}
|
|
67
|
+
op = body.call(thisArg, _);
|
|
68
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
69
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
73
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
74
|
+
};
|
|
75
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
76
|
+
exports.resolveInstructionsReturn = resolveInstructionsReturn;
|
|
77
|
+
var fs = __importStar(require("fs"));
|
|
78
|
+
var path = __importStar(require("path"));
|
|
79
|
+
var handlebars_1 = __importDefault(require("handlebars"));
|
|
80
|
+
var fileCache = new Map();
|
|
81
|
+
/**
|
|
82
|
+
* Resolve a file path relative to project root (process.cwd()).
|
|
83
|
+
* Leading `./` and `/` are normalised away — all paths are treated as project-root-relative.
|
|
84
|
+
*/
|
|
85
|
+
function resolveFilePath(filePath) {
|
|
86
|
+
// Strip leading slash so "/foo.md" behaves the same as "foo.md"
|
|
87
|
+
var normalised = filePath.replace(/^\/+/, "");
|
|
88
|
+
return path.resolve(process.cwd(), normalised);
|
|
89
|
+
}
|
|
90
|
+
function loadFile(filePath) {
|
|
91
|
+
var resolvedPath = resolveFilePath(filePath);
|
|
92
|
+
try {
|
|
93
|
+
var stats = fs.statSync(resolvedPath);
|
|
94
|
+
var mtime = stats.mtimeMs;
|
|
95
|
+
var cached = fileCache.get(resolvedPath);
|
|
96
|
+
if (cached && cached.mtime === mtime) {
|
|
97
|
+
return cached;
|
|
98
|
+
}
|
|
99
|
+
var content = fs.readFileSync(resolvedPath, "utf-8");
|
|
100
|
+
var entry = { content: content, mtime: mtime };
|
|
101
|
+
fileCache.set(resolvedPath, entry);
|
|
102
|
+
return entry;
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
if (err.code === "ENOENT") {
|
|
106
|
+
throw new Error("Agent instructions file not found: ".concat(resolvedPath, " (from: ").concat(filePath, ")"));
|
|
107
|
+
}
|
|
108
|
+
throw new Error("Failed to load agent instructions file: ".concat(resolvedPath, " - ").concat(err.message));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function renderTemplate(entry, data) {
|
|
112
|
+
if (entry.hasTemplateExpressions === false) {
|
|
113
|
+
return entry.content;
|
|
114
|
+
}
|
|
115
|
+
if (!entry.compiledTemplate) {
|
|
116
|
+
entry.hasTemplateExpressions = /\{\{/.test(entry.content);
|
|
117
|
+
if (!entry.hasTemplateExpressions) {
|
|
118
|
+
return entry.content;
|
|
119
|
+
}
|
|
120
|
+
entry.compiledTemplate = handlebars_1.default.compile(entry.content);
|
|
121
|
+
}
|
|
122
|
+
return entry.compiledTemplate(data);
|
|
123
|
+
}
|
|
124
|
+
var TEXT_FILE_EXTENSIONS = [".md", ".txt", ".yaml", ".yml", ".xml", ".toml", ".ini", ".json", ".html", ".htm"];
|
|
125
|
+
function isTextFilePath(value) {
|
|
126
|
+
var trimmed = value.trimEnd().toLowerCase();
|
|
127
|
+
return TEXT_FILE_EXTENSIONS.some(function (ext) { return trimmed.endsWith(ext); });
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Resolve an InstructionsReturn value to a plain string.
|
|
131
|
+
* @internal Used by FlinkAgent.toAgentProps()
|
|
132
|
+
*/
|
|
133
|
+
function resolveInstructionsReturn(result, ctx, agentContext) {
|
|
134
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
135
|
+
var entry_1, templateData_1, file, params, entry, templateData;
|
|
136
|
+
return __generator(this, function (_a) {
|
|
137
|
+
if (typeof result === "string") {
|
|
138
|
+
if (isTextFilePath(result)) {
|
|
139
|
+
entry_1 = loadFile(result.trimEnd());
|
|
140
|
+
templateData_1 = { ctx: ctx, agentContext: agentContext, user: agentContext === null || agentContext === void 0 ? void 0 : agentContext.user };
|
|
141
|
+
return [2 /*return*/, renderTemplate(entry_1, templateData_1)];
|
|
142
|
+
}
|
|
143
|
+
return [2 /*return*/, result];
|
|
144
|
+
}
|
|
145
|
+
file = result.file, params = result.params;
|
|
146
|
+
entry = loadFile(file);
|
|
147
|
+
templateData = __assign(__assign({}, params), { ctx: ctx, agentContext: agentContext, user: agentContext === null || agentContext === void 0 ? void 0 : agentContext.user });
|
|
148
|
+
return [2 /*return*/, renderTemplate(entry, templateData)];
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
}
|
package/package.json
CHANGED
|
@@ -166,7 +166,7 @@ describe("Agent Descendant Detection", () => {
|
|
|
166
166
|
export default class MyAgent extends InMemoryConversationAgent<any> {
|
|
167
167
|
id = "my-agent";
|
|
168
168
|
description = "Test agent";
|
|
169
|
-
instructions
|
|
169
|
+
instructions() { return "You are helpful"; }
|
|
170
170
|
}
|
|
171
171
|
`;
|
|
172
172
|
testAgentDetection(source, true);
|
|
@@ -182,7 +182,7 @@ describe("Agent Descendant Detection", () => {
|
|
|
182
182
|
export default class MyAgent extends InMemoryConversationAgent<AppContext> {
|
|
183
183
|
id = "my-agent";
|
|
184
184
|
description = "Test agent";
|
|
185
|
-
instructions
|
|
185
|
+
instructions() { return "You are helpful"; }
|
|
186
186
|
}
|
|
187
187
|
`;
|
|
188
188
|
testAgentDetection(source, true);
|
package/spec/AgentRunner.spec.ts
CHANGED
|
@@ -34,7 +34,7 @@ describe("Conversation Hooks", () => {
|
|
|
34
34
|
class TestAgent extends FlinkAgent<FlinkContext> {
|
|
35
35
|
id = "test-agent";
|
|
36
36
|
description = "Test agent";
|
|
37
|
-
instructions
|
|
37
|
+
instructions() { return "Test instructions"; }
|
|
38
38
|
tools: string[] = [];
|
|
39
39
|
|
|
40
40
|
protected beforeRun = beforeRunSpy;
|
|
@@ -71,7 +71,7 @@ describe("Conversation Hooks", () => {
|
|
|
71
71
|
class TestAgent extends FlinkAgent<FlinkContext> {
|
|
72
72
|
id = "test-agent";
|
|
73
73
|
description = "Test agent";
|
|
74
|
-
instructions
|
|
74
|
+
instructions() { return "Test instructions"; }
|
|
75
75
|
tools: string[] = [];
|
|
76
76
|
|
|
77
77
|
protected async beforeRun(input: AgentExecuteInput, context: AgentExecuteContext) {
|
|
@@ -139,7 +139,7 @@ describe("Conversation Hooks", () => {
|
|
|
139
139
|
class TestAgent extends FlinkAgent<FlinkContext> {
|
|
140
140
|
id = "test-agent";
|
|
141
141
|
description = "Test agent";
|
|
142
|
-
instructions
|
|
142
|
+
instructions() { return "Test instructions"; }
|
|
143
143
|
tools: string[] = ["test_tool"];
|
|
144
144
|
|
|
145
145
|
protected onStep = onStepSpy;
|
|
@@ -177,7 +177,7 @@ describe("Conversation Hooks", () => {
|
|
|
177
177
|
class TestAgent extends FlinkAgent<FlinkContext> {
|
|
178
178
|
id = "test-agent";
|
|
179
179
|
description = "Test agent";
|
|
180
|
-
instructions
|
|
180
|
+
instructions() { return "Test instructions"; }
|
|
181
181
|
tools: string[] = [];
|
|
182
182
|
|
|
183
183
|
protected afterRun = afterRunSpy;
|
|
@@ -216,7 +216,7 @@ describe("Conversation Hooks", () => {
|
|
|
216
216
|
class TestAgent extends FlinkAgent<FlinkContext> {
|
|
217
217
|
id = "test-agent";
|
|
218
218
|
description = "Test agent";
|
|
219
|
-
instructions
|
|
219
|
+
instructions() { return "Test instructions"; }
|
|
220
220
|
tools: string[] = [];
|
|
221
221
|
|
|
222
222
|
protected beforeRun = beforeRunSpy;
|
package/spec/FlinkAgent.spec.ts
CHANGED
|
@@ -31,7 +31,7 @@ describe("FlinkAgent", () => {
|
|
|
31
31
|
class TestAgent extends FlinkAgent<FlinkContext> {
|
|
32
32
|
id = "test-agent";
|
|
33
33
|
description = "Test agent";
|
|
34
|
-
instructions
|
|
34
|
+
instructions() { return "Test instructions"; }
|
|
35
35
|
tools: string[] = [];
|
|
36
36
|
|
|
37
37
|
async query(message: string) {
|
|
@@ -131,7 +131,7 @@ describe("FlinkAgent", () => {
|
|
|
131
131
|
class TestAgent extends FlinkAgent<FlinkContext> {
|
|
132
132
|
id = "test-agent";
|
|
133
133
|
description = "Test agent";
|
|
134
|
-
instructions
|
|
134
|
+
instructions() { return "Test instructions"; }
|
|
135
135
|
tools: string[] = ["public_tool", "admin_tool"];
|
|
136
136
|
|
|
137
137
|
async query(message: string) {
|
|
@@ -250,7 +250,7 @@ describe("FlinkAgent", () => {
|
|
|
250
250
|
class TestAgentWithToolRef extends FlinkAgent<FlinkContext> {
|
|
251
251
|
id = "test-agent";
|
|
252
252
|
description = "Test agent";
|
|
253
|
-
instructions
|
|
253
|
+
instructions() { return "Test instructions"; }
|
|
254
254
|
tools = [mockToolFile]; // Direct tool file reference!
|
|
255
255
|
|
|
256
256
|
async query(message: string) {
|
|
@@ -290,7 +290,7 @@ describe("FlinkAgent", () => {
|
|
|
290
290
|
class MixedToolsAgent extends FlinkAgent<FlinkContext> {
|
|
291
291
|
id = "test-agent";
|
|
292
292
|
description = "Test agent";
|
|
293
|
-
instructions
|
|
293
|
+
instructions() { return "Test instructions"; }
|
|
294
294
|
tools = [
|
|
295
295
|
mockToolFile, // File reference
|
|
296
296
|
"tool-by-string", // String ID
|
|
@@ -317,7 +317,7 @@ describe("FlinkAgent", () => {
|
|
|
317
317
|
class StaticAgent extends FlinkAgent<FlinkContext> {
|
|
318
318
|
id = "test-agent";
|
|
319
319
|
description = "Static agent";
|
|
320
|
-
instructions
|
|
320
|
+
instructions() { return "Static instructions"; }
|
|
321
321
|
tools: string[] = [];
|
|
322
322
|
|
|
323
323
|
async query(message: string) {
|
|
@@ -342,9 +342,9 @@ describe("FlinkAgent", () => {
|
|
|
342
342
|
class DynamicAgent extends FlinkAgent<FlinkContext> {
|
|
343
343
|
id = "test-agent";
|
|
344
344
|
description = "Dynamic agent";
|
|
345
|
-
instructions
|
|
345
|
+
instructions(ctx: FlinkContext, agentContext: any) {
|
|
346
346
|
return `You are a support agent for user ${agentContext.user?.name || "unknown"}`;
|
|
347
|
-
}
|
|
347
|
+
}
|
|
348
348
|
tools: string[] = [];
|
|
349
349
|
|
|
350
350
|
async query(message: string) {
|
|
@@ -380,13 +380,13 @@ describe("FlinkAgent", () => {
|
|
|
380
380
|
class DatabaseAgent extends FlinkAgent<any> {
|
|
381
381
|
id = "test-agent";
|
|
382
382
|
description = "Database agent";
|
|
383
|
-
|
|
383
|
+
async instructions(ctx: any, agentContext: any) {
|
|
384
384
|
if (!agentContext.user) {
|
|
385
385
|
return "You are a support agent.";
|
|
386
386
|
}
|
|
387
387
|
const profile = await ctx.repos.userRepo.getById(agentContext.user.id);
|
|
388
388
|
return `You are a support agent. Customer: ${profile.name}, Tier: ${profile.tier}`;
|
|
389
|
-
}
|
|
389
|
+
}
|
|
390
390
|
tools: string[] = [];
|
|
391
391
|
|
|
392
392
|
async query(message: string) {
|
|
@@ -415,10 +415,10 @@ describe("FlinkAgent", () => {
|
|
|
415
415
|
class CachedAgent extends FlinkAgent<FlinkContext> {
|
|
416
416
|
id = "test-agent";
|
|
417
417
|
description = "Cached agent";
|
|
418
|
-
instructions
|
|
418
|
+
instructions(ctx: FlinkContext, agentContext: any) {
|
|
419
419
|
callCount++;
|
|
420
420
|
return `Instructions for user ${agentContext.user?.name || "unknown"}`;
|
|
421
|
-
}
|
|
421
|
+
}
|
|
422
422
|
tools: string[] = [];
|
|
423
423
|
|
|
424
424
|
async query(message: string) {
|
|
@@ -461,10 +461,10 @@ describe("FlinkAgent", () => {
|
|
|
461
461
|
class ConversationAgent extends FlinkAgent<FlinkContext> {
|
|
462
462
|
id = "test-agent";
|
|
463
463
|
description = "Conversation agent";
|
|
464
|
-
instructions
|
|
464
|
+
instructions(ctx: FlinkContext, agentContext: any) {
|
|
465
465
|
capturedContext = agentContext;
|
|
466
466
|
return `Conversation: ${agentContext.conversationId || "new"}`;
|
|
467
|
-
}
|
|
467
|
+
}
|
|
468
468
|
tools: string[] = [];
|
|
469
469
|
|
|
470
470
|
async query(message: string, conversationId?: string) {
|
|
@@ -489,9 +489,9 @@ describe("FlinkAgent", () => {
|
|
|
489
489
|
class ErrorAgent extends FlinkAgent<FlinkContext> {
|
|
490
490
|
id = "test-agent";
|
|
491
491
|
description = "Error agent";
|
|
492
|
-
instructions
|
|
492
|
+
instructions(): string {
|
|
493
493
|
throw new Error("Database unavailable");
|
|
494
|
-
}
|
|
494
|
+
}
|
|
495
495
|
tools: string[] = [];
|
|
496
496
|
|
|
497
497
|
async query(message: string) {
|
|
@@ -518,7 +518,7 @@ describe("FlinkAgent", () => {
|
|
|
518
518
|
class TestAgent extends FlinkAgent<FlinkContext, TestConversationContext> {
|
|
519
519
|
id = "test-agent";
|
|
520
520
|
description = "Test agent";
|
|
521
|
-
instructions
|
|
521
|
+
instructions() { return "Test instructions"; }
|
|
522
522
|
tools: string[] = [];
|
|
523
523
|
|
|
524
524
|
async query(message: string) {
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { FlinkApp } from "../src/FlinkApp";
|
|
2
|
+
import { FlinkContext } from "../src/FlinkContext";
|
|
3
|
+
import { GetHandler, Handler, HttpMethod } from "../src/FlinkHttpHandler";
|
|
4
|
+
|
|
5
|
+
const request = require("supertest");
|
|
6
|
+
|
|
7
|
+
interface TestContext extends FlinkContext {}
|
|
8
|
+
|
|
9
|
+
const resSchema = {
|
|
10
|
+
type: "object",
|
|
11
|
+
properties: {
|
|
12
|
+
id: { type: "string" },
|
|
13
|
+
},
|
|
14
|
+
required: ["id"],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
describe("FlinkApp response validation when handler returns no data", () => {
|
|
18
|
+
let app: FlinkApp<TestContext>;
|
|
19
|
+
|
|
20
|
+
afterEach(async () => {
|
|
21
|
+
if (app && app.started) {
|
|
22
|
+
await app.stop();
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should return 500 bad response when handler returns undefined data with a response schema", async () => {
|
|
27
|
+
const handler: GetHandler<TestContext, any> = async () => {
|
|
28
|
+
return { status: 200 } as any; // no data
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
app = new FlinkApp<TestContext>({ name: "test-undefined-data", port: 4200 });
|
|
32
|
+
await app.start();
|
|
33
|
+
|
|
34
|
+
app.addHandler({
|
|
35
|
+
default: handler,
|
|
36
|
+
Route: { method: HttpMethod.get, path: "/test", resSchema },
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const response = await request(app.expressApp).get("/test");
|
|
40
|
+
|
|
41
|
+
expect(response.status).toBe(500);
|
|
42
|
+
expect(response.body.error.title).toBe("Bad response");
|
|
43
|
+
expect(response.body.error.detail).toContain("no data");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should NOT return 500 when handler returns status 204 with no data (even if schema is defined)", async () => {
|
|
47
|
+
const handler: Handler<TestContext, any, any> = async () => {
|
|
48
|
+
return { status: 204 } as any;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
app = new FlinkApp<TestContext>({ name: "test-204-no-data", port: 4201 });
|
|
52
|
+
await app.start();
|
|
53
|
+
|
|
54
|
+
app.addHandler({
|
|
55
|
+
default: handler,
|
|
56
|
+
Route: { method: HttpMethod.patch, path: "/test", resSchema },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const response = await request(app.expressApp).patch("/test");
|
|
60
|
+
|
|
61
|
+
expect(response.status).toBe(204);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("should validate normally when handler returns valid data", async () => {
|
|
65
|
+
const handler: GetHandler<TestContext, any> = async () => {
|
|
66
|
+
return { data: { id: "abc-123" } };
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
app = new FlinkApp<TestContext>({ name: "test-valid-data", port: 4202 });
|
|
70
|
+
await app.start();
|
|
71
|
+
|
|
72
|
+
app.addHandler({
|
|
73
|
+
default: handler,
|
|
74
|
+
Route: { method: HttpMethod.get, path: "/test", resSchema },
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const response = await request(app.expressApp).get("/test");
|
|
78
|
+
|
|
79
|
+
expect(response.status).toBe(200);
|
|
80
|
+
expect(response.body.data.id).toBe("abc-123");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should return 500 bad response when handler returns invalid data against schema", async () => {
|
|
84
|
+
const handler: GetHandler<TestContext, any> = async () => {
|
|
85
|
+
return { data: { wrongField: 123 } }; // missing required "id"
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
app = new FlinkApp<TestContext>({ name: "test-invalid-data", port: 4203 });
|
|
89
|
+
await app.start();
|
|
90
|
+
|
|
91
|
+
app.addHandler({
|
|
92
|
+
default: handler,
|
|
93
|
+
Route: { method: HttpMethod.get, path: "/test", resSchema },
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const response = await request(app.expressApp).get("/test");
|
|
97
|
+
|
|
98
|
+
expect(response.status).toBe(500);
|
|
99
|
+
expect(response.body.error.title).toBe("Bad response");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("should not crash the server when handler returns undefined data", async () => {
|
|
103
|
+
const handler: GetHandler<TestContext, any> = async () => {
|
|
104
|
+
return { status: 200 } as any;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
app = new FlinkApp<TestContext>({ name: "test-no-crash", port: 4204 });
|
|
108
|
+
await app.start();
|
|
109
|
+
|
|
110
|
+
app.addHandler({
|
|
111
|
+
default: handler,
|
|
112
|
+
Route: { method: HttpMethod.get, path: "/test", resSchema },
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// First request triggers the bad response
|
|
116
|
+
await request(app.expressApp).get("/test");
|
|
117
|
+
|
|
118
|
+
// Server should still be running and able to handle further requests
|
|
119
|
+
const second = await request(app.expressApp).get("/test");
|
|
120
|
+
expect(second.status).toBe(500); // still returns 500, but doesn't crash
|
|
121
|
+
expect(app.started).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { FlinkApp, autoRegisteredJobs } from "../src/FlinkApp";
|
|
2
|
+
import { FlinkContext } from "../src/FlinkContext";
|
|
3
|
+
import { FlinkJobFile } from "../src/FlinkJob";
|
|
4
|
+
|
|
5
|
+
interface TestContext extends FlinkContext {}
|
|
6
|
+
|
|
7
|
+
describe("FlinkJob error handling", () => {
|
|
8
|
+
let app: FlinkApp<TestContext>;
|
|
9
|
+
let consoleErrorSpy: jasmine.Spy;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
consoleErrorSpy = spyOn(console, "error");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
autoRegisteredJobs.length = 0;
|
|
17
|
+
if (app?.started) {
|
|
18
|
+
await app.stop();
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should catch and log errors from afterDelay 0ms jobs without crashing", async () => {
|
|
23
|
+
const job: FlinkJobFile = {
|
|
24
|
+
Job: { id: "failing-job-0ms", afterDelay: "0ms" },
|
|
25
|
+
default: async () => {
|
|
26
|
+
throw new Error("Job error 0ms");
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
autoRegisteredJobs.push(job);
|
|
31
|
+
|
|
32
|
+
app = new FlinkApp<TestContext>({ name: "test-job-errors-0ms", disableHttpServer: true });
|
|
33
|
+
await app.start();
|
|
34
|
+
|
|
35
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
36
|
+
|
|
37
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should catch and log errors from afterDelay jobs without crashing", async () => {
|
|
41
|
+
const job: FlinkJobFile = {
|
|
42
|
+
Job: { id: "failing-job-delay", afterDelay: "10ms" },
|
|
43
|
+
default: async () => {
|
|
44
|
+
throw new Error("Job error with delay");
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
autoRegisteredJobs.push(job);
|
|
49
|
+
|
|
50
|
+
app = new FlinkApp<TestContext>({ name: "test-job-errors-delay", disableHttpServer: true });
|
|
51
|
+
await app.start();
|
|
52
|
+
|
|
53
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
54
|
+
|
|
55
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should catch and log errors from interval jobs without crashing", async () => {
|
|
59
|
+
const job: FlinkJobFile = {
|
|
60
|
+
Job: { id: "failing-interval-job", interval: "10ms" },
|
|
61
|
+
default: async () => {
|
|
62
|
+
throw new Error("Interval job error");
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
autoRegisteredJobs.push(job);
|
|
67
|
+
|
|
68
|
+
app = new FlinkApp<TestContext>({ name: "test-job-errors-interval", disableHttpServer: true });
|
|
69
|
+
await app.start();
|
|
70
|
+
|
|
71
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
72
|
+
|
|
73
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("should run afterDelay 0ms job exactly once", async () => {
|
|
77
|
+
let runCount = 0;
|
|
78
|
+
|
|
79
|
+
const job: FlinkJobFile = {
|
|
80
|
+
Job: { id: "once-job-0ms", afterDelay: "0ms" },
|
|
81
|
+
default: async () => {
|
|
82
|
+
runCount++;
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
autoRegisteredJobs.push(job);
|
|
87
|
+
|
|
88
|
+
app = new FlinkApp<TestContext>({ name: "test-job-once-0ms", disableHttpServer: true });
|
|
89
|
+
await app.start();
|
|
90
|
+
|
|
91
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
92
|
+
|
|
93
|
+
expect(runCount).toBe(1);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -15,7 +15,7 @@ describe("Streaming Integration", () => {
|
|
|
15
15
|
class StreamingTestAgent extends FlinkAgent<FlinkContext> {
|
|
16
16
|
id = "streaming-test-agent";
|
|
17
17
|
description = "Test agent for streaming";
|
|
18
|
-
instructions
|
|
18
|
+
instructions() { return "You are a test agent"; }
|
|
19
19
|
tools = [];
|
|
20
20
|
|
|
21
21
|
async query(message: string) {
|
|
@@ -7,7 +7,7 @@ import { LLMAdapter, LLMMessage } from "../../src/ai/LLMAdapter";
|
|
|
7
7
|
class CompactingAgent extends FlinkAgent<FlinkContext> {
|
|
8
8
|
id = "compacting-agent";
|
|
9
9
|
description = "Test agent with context compaction";
|
|
10
|
-
instructions
|
|
10
|
+
instructions() { return "You are a test assistant"; }
|
|
11
11
|
|
|
12
12
|
setCallbacks(
|
|
13
13
|
shouldCompact?: (messages: LLMMessage[], step: number) => boolean | Promise<boolean>,
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
class MockConversationAgent extends ConversationAgent<FlinkContext> {
|
|
10
10
|
id = "mock-agent";
|
|
11
11
|
description = "Mock agent for testing";
|
|
12
|
-
instructions
|
|
12
|
+
instructions() { return "You are a test agent"; }
|
|
13
13
|
tools = [];
|
|
14
14
|
|
|
15
15
|
// Spy methods
|
|
@@ -3,7 +3,7 @@ import { InMemoryConversationAgent, FlinkContext } from "../../src";
|
|
|
3
3
|
class TestAgent extends InMemoryConversationAgent<FlinkContext> {
|
|
4
4
|
id = "test-agent";
|
|
5
5
|
description = "Test agent";
|
|
6
|
-
instructions
|
|
6
|
+
instructions() { return "You are a test agent"; }
|
|
7
7
|
tools = [];
|
|
8
8
|
}
|
|
9
9
|
|
|
@@ -8,15 +8,17 @@ export default class TestAgent extends FlinkAgent<FlinkContext> {
|
|
|
8
8
|
// Load instructions from file using project-root path
|
|
9
9
|
// Note: For relative paths (./) to work, they must be called from the agent file itself
|
|
10
10
|
// Since this is loaded via require() from test, we use project-root path
|
|
11
|
-
instructions
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
11
|
+
instructions(ctx: FlinkContext, agentContext: any) {
|
|
12
|
+
return agentInstructions(
|
|
13
|
+
"spec/fixtures/agent-instructions/template.md",
|
|
14
|
+
(ctx, agentContext) => ({
|
|
15
|
+
tier: agentContext.user?.tier || "basic",
|
|
16
|
+
isPremium: agentContext.user?.tier === "premium",
|
|
17
|
+
isBusinessHours: true,
|
|
18
|
+
tools: ["test-tool-1", "test-tool-2"],
|
|
19
|
+
})
|
|
20
|
+
)(ctx, agentContext);
|
|
21
|
+
}
|
|
20
22
|
|
|
21
23
|
tools = [];
|
|
22
24
|
}
|