@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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# @flink-app/flink
|
|
2
2
|
|
|
3
|
+
## 2.0.0-alpha.63
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 810df2c: Fix FlinkJob afterDelay: "0ms" running in a tight loop instead of once. Zero-delay jobs now run immediately via setImmediate (exactly once) rather than being registered with a 0ms scheduler interval. Also adds specs covering uncaught exception handling for all job scheduling modes.
|
|
8
|
+
- 8dd0752: fix: prevent server crash when handler returns no data with a response schema defined
|
|
9
|
+
|
|
10
|
+
When a PATCH (or any) handler returned `{ status: 204 }` without a `data` field,
|
|
11
|
+
`JSON.stringify(undefined)` returned the JS value `undefined`, causing
|
|
12
|
+
`JSON.parse(undefined)` to throw a `SyntaxError` that crashed the entire server process.
|
|
13
|
+
|
|
14
|
+
New behaviour:
|
|
15
|
+
|
|
16
|
+
- Status 204 + no data → skip validation silently (intentional no-content response)
|
|
17
|
+
- Non-204 status + no data + response schema defined → return 500 bad response with a
|
|
18
|
+
descriptive error message (surfaces the developer mistake without crashing the server)
|
|
19
|
+
- Data present → validate against schema as before
|
|
20
|
+
|
|
21
|
+
## 2.0.0-alpha.62
|
|
22
|
+
|
|
23
|
+
### Minor Changes
|
|
24
|
+
|
|
25
|
+
- Add file-based agent instructions with Handlebars templating
|
|
26
|
+
|
|
27
|
+
The `instructions()` method now supports returning a file path or `{ file, params? }` object:
|
|
28
|
+
|
|
29
|
+
- Return a string ending in `.md`, `.txt`, `.yaml`, `.yml`, `.xml`, etc. to auto-load from disk
|
|
30
|
+
- Return `{ file: "instructions/agent.md", params: { tier: "premium" } }` for Handlebars templates
|
|
31
|
+
- All paths resolve relative to the project root (`process.cwd()`)
|
|
32
|
+
- Files are cached with mtime-based invalidation
|
|
33
|
+
- Template variables `{{ctx}}`, `{{agentContext}}`, and `{{user}}` are always available
|
|
34
|
+
|
|
35
|
+
Also fixes a bug where `getRequestPermissions()` returning `[]` outside a request context
|
|
36
|
+
would override `user.permissions` in the fallback chain, causing permission checks to fail.
|
|
37
|
+
|
|
3
38
|
## 2.0.0-alpha.61
|
|
4
39
|
|
|
5
40
|
## 2.0.0-alpha.60
|
package/dist/src/FlinkApp.js
CHANGED
|
@@ -423,7 +423,7 @@ var FlinkApp = /** @class */ (function () {
|
|
|
423
423
|
}
|
|
424
424
|
}
|
|
425
425
|
this.expressApp[method](routeProps.path, function (req, res) { return __awaiter(_this, void 0, void 0, function () {
|
|
426
|
-
var valid, formattedErrors, data, normalizedQuery, _i, _a, _b, key, value, stream, handlerRes, flinkReq_1, err_1, errorResponse, result, valid, formattedErrors;
|
|
426
|
+
var valid, formattedErrors, data, normalizedQuery, _i, _a, _b, key, value, stream, handlerRes, flinkReq_1, err_1, errorResponse, result, detail, valid, formattedErrors;
|
|
427
427
|
var _this = this;
|
|
428
428
|
return __generator(this, function (_c) {
|
|
429
429
|
switch (_c.label) {
|
|
@@ -570,18 +570,30 @@ var FlinkApp = /** @class */ (function () {
|
|
|
570
570
|
return [2 /*return*/, res.status(204).send()];
|
|
571
571
|
}
|
|
572
572
|
if (validateRes_1 && !(0, utils_1.isError)(handlerRes)) {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
573
|
+
if (handlerRes.data === undefined) {
|
|
574
|
+
if (handlerRes.status !== 204) {
|
|
575
|
+
detail = "Response schema is defined but handler returned no data";
|
|
576
|
+
FlinkLog_1.log.warn("[".concat(req.reqId, "] ").concat(methodAndRoute_1, ": Bad response - ").concat(detail));
|
|
577
|
+
return [2 /*return*/, res.status(500).json({
|
|
578
|
+
status: 500,
|
|
579
|
+
error: { id: (0, uuid_1.v4)(), title: "Bad response", detail: detail },
|
|
580
|
+
})];
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
valid = validateRes_1(JSON.parse(JSON.stringify(handlerRes.data)));
|
|
585
|
+
if (!valid) {
|
|
586
|
+
formattedErrors = (0, utils_1.formatValidationErrors)(validateRes_1.errors, handlerRes.data);
|
|
587
|
+
FlinkLog_1.log.warn("[".concat(req.reqId, "] ").concat(methodAndRoute_1, ": Bad response\n").concat(formattedErrors));
|
|
588
|
+
return [2 /*return*/, res.status(500).json({
|
|
589
|
+
status: 500,
|
|
590
|
+
error: {
|
|
591
|
+
id: (0, uuid_1.v4)(),
|
|
592
|
+
title: "Bad response",
|
|
593
|
+
detail: formattedErrors,
|
|
594
|
+
},
|
|
595
|
+
})];
|
|
596
|
+
}
|
|
585
597
|
}
|
|
586
598
|
}
|
|
587
599
|
res.set(handlerRes.headers);
|
|
@@ -901,14 +913,38 @@ var FlinkApp = /** @class */ (function () {
|
|
|
901
913
|
this_1.scheduler.addSimpleIntervalJob(job);
|
|
902
914
|
}
|
|
903
915
|
else if (jobProps.afterDelay !== undefined) {
|
|
904
|
-
var
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
916
|
+
var delayMs = (0, ms_1.default)(jobProps.afterDelay);
|
|
917
|
+
if (delayMs === 0) {
|
|
918
|
+
setImmediate(function () { return __awaiter(_this, void 0, void 0, function () {
|
|
919
|
+
var err_2;
|
|
920
|
+
return __generator(this, function (_a) {
|
|
921
|
+
switch (_a.label) {
|
|
922
|
+
case 0:
|
|
923
|
+
_a.trys.push([0, 2, , 3]);
|
|
924
|
+
return [4 /*yield*/, jobFn({ ctx: this.ctx })];
|
|
925
|
+
case 1:
|
|
926
|
+
_a.sent();
|
|
927
|
+
return [3 /*break*/, 3];
|
|
928
|
+
case 2:
|
|
929
|
+
err_2 = _a.sent();
|
|
930
|
+
FlinkLog_1.log.error("Job ".concat(jobProps.id, " threw unhandled exception ").concat(err_2));
|
|
931
|
+
console.error(err_2);
|
|
932
|
+
return [3 /*break*/, 3];
|
|
933
|
+
case 3: return [2 /*return*/];
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
}); });
|
|
937
|
+
}
|
|
938
|
+
else {
|
|
939
|
+
var job = new toad_scheduler_1.SimpleIntervalJob({
|
|
940
|
+
milliseconds: delayMs,
|
|
941
|
+
runImmediately: false,
|
|
942
|
+
}, task, {
|
|
943
|
+
id: jobProps.id,
|
|
944
|
+
preventOverrun: jobProps.singleton,
|
|
945
|
+
});
|
|
946
|
+
this_1.scheduler.addSimpleIntervalJob(job);
|
|
947
|
+
}
|
|
912
948
|
}
|
|
913
949
|
else {
|
|
914
950
|
FlinkLog_1.log.error("Cannot register job ".concat(jobProps.id, " - no cron, interval or once set in ").concat(__file));
|
|
@@ -931,7 +967,7 @@ var FlinkApp = /** @class */ (function () {
|
|
|
931
967
|
};
|
|
932
968
|
FlinkApp.prototype.registerAutoRegisterableTools = function () {
|
|
933
969
|
return __awaiter(this, void 0, void 0, function () {
|
|
934
|
-
var ToolExecutor, getRepoInstanceName, schemaManifest, _i, autoRegisteredTools_1, toolFile, toolId, toolInstanceName, metadata, schemas, toolExecutor;
|
|
970
|
+
var ToolExecutor, getRepoInstanceName, schemaManifest, _i, autoRegisteredTools_1, toolFile, toolId, toolInstanceName, metadata, schemas, allSchemas, toolExecutor;
|
|
935
971
|
return __generator(this, function (_a) {
|
|
936
972
|
ToolExecutor = require("./ai/ToolExecutor").ToolExecutor;
|
|
937
973
|
getRepoInstanceName = require("./utils").getRepoInstanceName;
|
|
@@ -968,8 +1004,9 @@ var FlinkApp = /** @class */ (function () {
|
|
|
968
1004
|
if ((metadata === null || metadata === void 0 ? void 0 : metadata.outputSchemaName) && !(schemas === null || schemas === void 0 ? void 0 : schemas.outputSchema)) {
|
|
969
1005
|
FlinkLog_1.log.warn("Tool ".concat(toolFile.__file, " references outputSchema \"").concat(metadata.outputSchemaName, "\" but not found in schema universe"));
|
|
970
1006
|
}
|
|
971
|
-
|
|
972
|
-
)
|
|
1007
|
+
allSchemas = schemaManifest.version === "2.0" ? schemaManifest.schemas : schemaManifest.definitions;
|
|
1008
|
+
toolExecutor = new ToolExecutor(toolFile.Tool, toolFile.default, this.ctx, schemas, // Auto-generated schemas from manifest (resolved from definitions)
|
|
1009
|
+
allSchemas);
|
|
973
1010
|
this.tools[toolInstanceName] = toolExecutor;
|
|
974
1011
|
initLog.info("Registered tool ".concat(toolInstanceName, " (").concat(toolId, ")"));
|
|
975
1012
|
}
|
|
@@ -1110,7 +1147,7 @@ var FlinkApp = /** @class */ (function () {
|
|
|
1110
1147
|
*/
|
|
1111
1148
|
FlinkApp.prototype.initDb = function () {
|
|
1112
1149
|
return __awaiter(this, void 0, void 0, function () {
|
|
1113
|
-
var client,
|
|
1150
|
+
var client, err_3;
|
|
1114
1151
|
return __generator(this, function (_a) {
|
|
1115
1152
|
switch (_a.label) {
|
|
1116
1153
|
case 0:
|
|
@@ -1126,8 +1163,8 @@ var FlinkApp = /** @class */ (function () {
|
|
|
1126
1163
|
this.dbClient = client;
|
|
1127
1164
|
return [3 /*break*/, 4];
|
|
1128
1165
|
case 3:
|
|
1129
|
-
|
|
1130
|
-
FlinkLog_1.log.error("Failed to connect to db: " +
|
|
1166
|
+
err_3 = _a.sent();
|
|
1167
|
+
FlinkLog_1.log.error("Failed to connect to db: " + err_3);
|
|
1131
1168
|
process.exit(1);
|
|
1132
1169
|
return [3 /*break*/, 4];
|
|
1133
1170
|
case 4:
|
|
@@ -1146,7 +1183,7 @@ var FlinkApp = /** @class */ (function () {
|
|
|
1146
1183
|
*/
|
|
1147
1184
|
FlinkApp.prototype.initPluginDb = function (plugin) {
|
|
1148
1185
|
return __awaiter(this, void 0, void 0, function () {
|
|
1149
|
-
var client,
|
|
1186
|
+
var client, err_4;
|
|
1150
1187
|
return __generator(this, function (_a) {
|
|
1151
1188
|
switch (_a.label) {
|
|
1152
1189
|
case 0:
|
|
@@ -1173,8 +1210,8 @@ var FlinkApp = /** @class */ (function () {
|
|
|
1173
1210
|
client = _a.sent();
|
|
1174
1211
|
return [2 /*return*/, client.db()];
|
|
1175
1212
|
case 4:
|
|
1176
|
-
|
|
1177
|
-
FlinkLog_1.log.error("Failed to connect to db defined in plugin '".concat(plugin.id, "': ") +
|
|
1213
|
+
err_4 = _a.sent();
|
|
1214
|
+
FlinkLog_1.log.error("Failed to connect to db defined in plugin '".concat(plugin.id, "': ") + err_4);
|
|
1178
1215
|
return [3 /*break*/, 5];
|
|
1179
1216
|
case 5: return [2 /*return*/];
|
|
1180
1217
|
}
|
package/dist/src/FlinkRepo.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export declare abstract class FlinkRepo<C extends FlinkContext, Model extends Do
|
|
|
14
14
|
collection: Collection;
|
|
15
15
|
private _ctx?;
|
|
16
16
|
set ctx(ctx: FlinkContext);
|
|
17
|
-
get ctx():
|
|
17
|
+
get ctx(): C;
|
|
18
18
|
constructor(collectionName: string, db: Db, client?: MongoClient | undefined);
|
|
19
19
|
findAll(query?: {}): Promise<Model[]>;
|
|
20
20
|
getById(id: string | ObjectId): Promise<Model | null>;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { FlinkContext } from "../FlinkContext";
|
|
2
2
|
import { FlinkToolFile, FlinkToolProps } from "./FlinkTool";
|
|
3
|
+
import { type InstructionsReturn } from "./instructionFileLoader";
|
|
4
|
+
export type { InstructionsReturn } from "./instructionFileLoader";
|
|
3
5
|
import { LLMContentBlock, LLMMessage } from "./LLMAdapter";
|
|
4
6
|
import { ToolExecutor } from "./ToolExecutor";
|
|
5
7
|
/**
|
|
@@ -241,7 +243,49 @@ export declare abstract class FlinkAgent<Ctx extends FlinkContext, ConversationC
|
|
|
241
243
|
private _tools?;
|
|
242
244
|
abstract id: string;
|
|
243
245
|
abstract description: string;
|
|
244
|
-
|
|
246
|
+
/**
|
|
247
|
+
* Define the agent's instructions. Override this method in your agent subclass.
|
|
248
|
+
*
|
|
249
|
+
* `ctx` is automatically typed as your app's context — no annotation needed.
|
|
250
|
+
*
|
|
251
|
+
* Supported return values:
|
|
252
|
+
* - `string` — Plain text used as-is
|
|
253
|
+
* - `string` ending with a known text extension (`.md`, `.txt`, `.yaml`, `.yml`, `.xml`, …) — Auto-loaded from disk (project-root-relative)
|
|
254
|
+
* - `{ file, params? }` — Explicitly load a file (any extension) with optional Handlebars template params
|
|
255
|
+
*
|
|
256
|
+
* @example Plain text
|
|
257
|
+
* instructions() {
|
|
258
|
+
* return "You are a helpful car assistant.";
|
|
259
|
+
* }
|
|
260
|
+
*
|
|
261
|
+
* @example Dynamic instructions using ctx and agentContext
|
|
262
|
+
* async instructions(ctx, agentContext) {
|
|
263
|
+
* const user = await ctx.repos.userRepo.getById(agentContext.user?.id);
|
|
264
|
+
* return `You are a support agent for ${user.name}.`;
|
|
265
|
+
* }
|
|
266
|
+
*
|
|
267
|
+
* @example Auto-load file by extension (.md, .txt, .yaml, .yml, .xml, … — path relative to project root)
|
|
268
|
+
* instructions() {
|
|
269
|
+
* return "src/agents/instructions/car-agent.md";
|
|
270
|
+
* }
|
|
271
|
+
*
|
|
272
|
+
* @example File with template params
|
|
273
|
+
* async instructions(ctx, agentContext) {
|
|
274
|
+
* return {
|
|
275
|
+
* file: "src/agents/instructions/support.md",
|
|
276
|
+
* params: {
|
|
277
|
+
* customerTier: agentContext.user?.tier || "standard",
|
|
278
|
+
* isBusinessHours: new Date().getHours() >= 9,
|
|
279
|
+
* },
|
|
280
|
+
* };
|
|
281
|
+
* }
|
|
282
|
+
*
|
|
283
|
+
* @example Agent-file-relative path (use agentInstructions helper)
|
|
284
|
+
* instructions(ctx, agentContext) {
|
|
285
|
+
* return agentInstructions("./instructions/support.md", { date: new Date() })(ctx, agentContext);
|
|
286
|
+
* }
|
|
287
|
+
*/
|
|
288
|
+
abstract instructions(ctx: Ctx, agentContext: AgentExecuteContext): Promise<InstructionsReturn> | InstructionsReturn;
|
|
245
289
|
tools?: Array<string | FlinkToolFile | FlinkToolProps>;
|
|
246
290
|
model?: {
|
|
247
291
|
adapterId?: string;
|
|
@@ -70,8 +70,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
70
70
|
exports.FlinkAgent = void 0;
|
|
71
71
|
var FlinkErrors_1 = require("../FlinkErrors");
|
|
72
72
|
var FlinkLogFactory_1 = require("../FlinkLogFactory");
|
|
73
|
+
var FlinkRequestContext_1 = require("../FlinkRequestContext");
|
|
73
74
|
var AgentRunner_1 = require("./AgentRunner");
|
|
75
|
+
var instructionFileLoader_1 = require("./instructionFileLoader");
|
|
74
76
|
var logger = FlinkLogFactory_1.FlinkLogFactory.createLogger("flink.ai.flink-agent");
|
|
77
|
+
var instructionsLog = FlinkLogFactory_1.FlinkLogFactory.createLogger("flink.ai.instructions");
|
|
75
78
|
/**
|
|
76
79
|
* Base class for Flink agents (similar to FlinkRepo pattern)
|
|
77
80
|
*
|
|
@@ -340,11 +343,11 @@ var FlinkAgent = /** @class */ (function () {
|
|
|
340
343
|
*/
|
|
341
344
|
FlinkAgent.prototype.execute = function (input) {
|
|
342
345
|
var _this = this;
|
|
343
|
-
var _a, _b, _c;
|
|
344
|
-
// Use bound user if not explicitly provided in input
|
|
345
|
-
var user = (_a = input.user) !== null && _a !== void 0 ? _a : this._boundUser;
|
|
346
|
-
var userPermissions = (
|
|
347
|
-
var conversationContext = (
|
|
346
|
+
var _a, _b, _c, _d, _e, _f;
|
|
347
|
+
// Use bound user if not explicitly provided in input, fall back to AsyncLocalStorage request context
|
|
348
|
+
var user = (_b = (_a = input.user) !== null && _a !== void 0 ? _a : this._boundUser) !== null && _b !== void 0 ? _b : (0, FlinkRequestContext_1.getRequestUser)();
|
|
349
|
+
var userPermissions = (_d = (_c = input.userPermissions) !== null && _c !== void 0 ? _c : this._boundUserPermissions) !== null && _d !== void 0 ? _d : (_e = (0, FlinkRequestContext_1.getRequestContext)()) === null || _e === void 0 ? void 0 : _e.userPermissions;
|
|
350
|
+
var conversationContext = (_f = input.conversationContext) !== null && _f !== void 0 ? _f : this._boundConversationContext;
|
|
348
351
|
var executeInput = __assign(__assign({}, input), { user: user, userPermissions: userPermissions, conversationContext: conversationContext });
|
|
349
352
|
// Permission check
|
|
350
353
|
if (this.permissions) {
|
|
@@ -620,11 +623,26 @@ var FlinkAgent = /** @class */ (function () {
|
|
|
620
623
|
return this.id;
|
|
621
624
|
};
|
|
622
625
|
FlinkAgent.prototype.toAgentProps = function () {
|
|
626
|
+
var _this = this;
|
|
623
627
|
var _a, _b, _c, _d, _e;
|
|
624
628
|
return {
|
|
625
629
|
id: this.getAgentId(),
|
|
626
630
|
description: this.description,
|
|
627
|
-
instructions:
|
|
631
|
+
instructions: function (ctx, agentContext) { return __awaiter(_this, void 0, void 0, function () {
|
|
632
|
+
var resolved, _a;
|
|
633
|
+
return __generator(this, function (_b) {
|
|
634
|
+
switch (_b.label) {
|
|
635
|
+
case 0:
|
|
636
|
+
_a = instructionFileLoader_1.resolveInstructionsReturn;
|
|
637
|
+
return [4 /*yield*/, Promise.resolve(this.instructions(ctx, agentContext))];
|
|
638
|
+
case 1: return [4 /*yield*/, _a.apply(void 0, [_b.sent(), ctx, agentContext])];
|
|
639
|
+
case 2:
|
|
640
|
+
resolved = _b.sent();
|
|
641
|
+
instructionsLog.debug("[".concat(this.getAgentId(), "] Resolved instructions:\n").concat(resolved));
|
|
642
|
+
return [2 /*return*/, resolved];
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
}); },
|
|
628
646
|
tools: this.tools,
|
|
629
647
|
model: this.model,
|
|
630
648
|
limits: this.limits,
|
|
@@ -7,12 +7,14 @@ export declare class ToolExecutor<Ctx extends FlinkContext> {
|
|
|
7
7
|
private ctx;
|
|
8
8
|
private autoSchemas?;
|
|
9
9
|
private ajv;
|
|
10
|
+
private compiledInputValidator?;
|
|
11
|
+
private compiledOutputValidator?;
|
|
10
12
|
constructor(toolProps: FlinkToolProps, toolFn: FlinkTool<Ctx, any, any>, ctx: Ctx, autoSchemas?: {
|
|
11
13
|
inputSchema?: any;
|
|
12
14
|
outputSchema?: any;
|
|
13
15
|
inputTypeHint?: "void" | "any" | "named";
|
|
14
16
|
outputTypeHint?: "void" | "any" | "named";
|
|
15
|
-
} | undefined);
|
|
17
|
+
} | undefined, allSchemas?: Record<string, any>);
|
|
16
18
|
/**
|
|
17
19
|
* Execute the tool with input
|
|
18
20
|
* @param input - Tool input data
|
|
@@ -46,12 +46,39 @@ var FlinkLogFactory_1 = require("../FlinkLogFactory");
|
|
|
46
46
|
var FlinkRequestContext_1 = require("../FlinkRequestContext");
|
|
47
47
|
var toolLog = FlinkLogFactory_1.FlinkLogFactory.createLogger("flink.ai.tool");
|
|
48
48
|
var ToolExecutor = /** @class */ (function () {
|
|
49
|
-
function ToolExecutor(toolProps, toolFn, ctx, autoSchemas) {
|
|
49
|
+
function ToolExecutor(toolProps, toolFn, ctx, autoSchemas, allSchemas) {
|
|
50
50
|
this.toolProps = toolProps;
|
|
51
51
|
this.toolFn = toolFn;
|
|
52
52
|
this.ctx = ctx;
|
|
53
53
|
this.autoSchemas = autoSchemas;
|
|
54
54
|
this.ajv = new ajv_1.default({ allErrors: true });
|
|
55
|
+
// Pre-populate AJV with all schemas so $ref references resolve across schema boundaries
|
|
56
|
+
if (allSchemas) {
|
|
57
|
+
for (var _i = 0, _a = Object.values(allSchemas); _i < _a.length; _i++) {
|
|
58
|
+
var schema = _a[_i];
|
|
59
|
+
if (schema && schema.$id) {
|
|
60
|
+
try {
|
|
61
|
+
this.ajv.addSchema(schema);
|
|
62
|
+
}
|
|
63
|
+
catch (_b) {
|
|
64
|
+
// Ignore duplicate schema errors (schema may already be added)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Pre-compile validators once at construction time (not per invocation)
|
|
70
|
+
if (toolProps.inputJsonSchema) {
|
|
71
|
+
this.compiledInputValidator = this.ajv.compile(toolProps.inputJsonSchema);
|
|
72
|
+
}
|
|
73
|
+
else if (autoSchemas === null || autoSchemas === void 0 ? void 0 : autoSchemas.inputSchema) {
|
|
74
|
+
this.compiledInputValidator = this.ajv.compile(autoSchemas.inputSchema);
|
|
75
|
+
}
|
|
76
|
+
if (toolProps.outputJsonSchema) {
|
|
77
|
+
this.compiledOutputValidator = this.ajv.compile(toolProps.outputJsonSchema);
|
|
78
|
+
}
|
|
79
|
+
else if (autoSchemas === null || autoSchemas === void 0 ? void 0 : autoSchemas.outputSchema) {
|
|
80
|
+
this.compiledOutputValidator = this.ajv.compile(autoSchemas.outputSchema);
|
|
81
|
+
}
|
|
55
82
|
// Log when using auto-schemas
|
|
56
83
|
if ((autoSchemas === null || autoSchemas === void 0 ? void 0 : autoSchemas.inputSchema) && !toolProps.inputSchema && !toolProps.inputJsonSchema) {
|
|
57
84
|
toolLog.debug("Tool ".concat(toolProps.id, ": Using auto-generated schemas from type parameters"));
|
|
@@ -80,34 +107,33 @@ var ToolExecutor = /** @class */ (function () {
|
|
|
80
107
|
*/
|
|
81
108
|
ToolExecutor.prototype.execute = function (input, overrides) {
|
|
82
109
|
return __awaiter(this, void 0, void 0, function () {
|
|
83
|
-
var user, userPermissions, conversationContext, hasPermission, validatedInput,
|
|
84
|
-
var _a, _b, _c
|
|
85
|
-
return __generator(this, function (
|
|
86
|
-
switch (
|
|
110
|
+
var user, userPermissions, conversationContext, hasPermission, validatedInput, valid, errorDetails, errorDetails, result, err_1, validatedData, valid, errorDetails;
|
|
111
|
+
var _a, _b, _c;
|
|
112
|
+
return __generator(this, function (_d) {
|
|
113
|
+
switch (_d.label) {
|
|
87
114
|
case 0:
|
|
88
115
|
user = (_a = overrides === null || overrides === void 0 ? void 0 : overrides.user) !== null && _a !== void 0 ? _a : (0, FlinkRequestContext_1.getRequestUser)();
|
|
89
|
-
userPermissions = (_b = overrides === null || overrides === void 0 ? void 0 : overrides.permissions) !== null && _b !== void 0 ? _b : (0, FlinkRequestContext_1.
|
|
116
|
+
userPermissions = (_b = overrides === null || overrides === void 0 ? void 0 : overrides.permissions) !== null && _b !== void 0 ? _b : (_c = (0, FlinkRequestContext_1.getRequestContext)()) === null || _c === void 0 ? void 0 : _c.userPermissions;
|
|
90
117
|
conversationContext = overrides === null || overrides === void 0 ? void 0 : overrides.conversationContext;
|
|
91
118
|
if (!this.toolProps.permissions) return [3 /*break*/, 2];
|
|
92
119
|
return [4 /*yield*/, this.checkPermissionsInternal(input, user, userPermissions)];
|
|
93
120
|
case 1:
|
|
94
|
-
hasPermission =
|
|
121
|
+
hasPermission = _d.sent();
|
|
95
122
|
if (!hasPermission) {
|
|
96
123
|
toolLog.debug("Tool invocator is missing required permission(s)", this.toolProps.permissions, "user has", userPermissions);
|
|
97
124
|
throw (0, FlinkErrors_1.forbidden)("Permission denied for tool ".concat(this.toolProps.id), "PERMISSION_DENIED");
|
|
98
125
|
}
|
|
99
|
-
|
|
126
|
+
_d.label = 2;
|
|
100
127
|
case 2:
|
|
101
128
|
try {
|
|
102
129
|
if (this.toolProps.inputSchema) {
|
|
103
130
|
// Priority 1: Use Zod validation
|
|
104
131
|
validatedInput = this.toolProps.inputSchema.parse(input);
|
|
105
132
|
}
|
|
106
|
-
else if (this.
|
|
107
|
-
|
|
108
|
-
valid = validate(input);
|
|
133
|
+
else if (this.compiledInputValidator) {
|
|
134
|
+
valid = this.compiledInputValidator(input);
|
|
109
135
|
if (!valid) {
|
|
110
|
-
errorDetails = this.formatAjvErrors(
|
|
136
|
+
errorDetails = this.formatAjvErrors(this.compiledInputValidator.errors || [], input);
|
|
111
137
|
toolLog.warn("Tool ".concat(this.toolProps.id, " input validation failed:"), errorDetails);
|
|
112
138
|
return [2 /*return*/, {
|
|
113
139
|
success: false,
|
|
@@ -117,20 +143,6 @@ var ToolExecutor = /** @class */ (function () {
|
|
|
117
143
|
}
|
|
118
144
|
validatedInput = input;
|
|
119
145
|
}
|
|
120
|
-
else if ((_c = this.autoSchemas) === null || _c === void 0 ? void 0 : _c.inputSchema) {
|
|
121
|
-
validate = this.ajv.compile(this.autoSchemas.inputSchema);
|
|
122
|
-
valid = validate(input);
|
|
123
|
-
if (!valid) {
|
|
124
|
-
errorDetails = this.formatAjvErrors(validate.errors || [], input);
|
|
125
|
-
toolLog.warn("Tool ".concat(this.toolProps.id, " input validation failed (auto-generated schema):"), errorDetails);
|
|
126
|
-
return [2 /*return*/, {
|
|
127
|
-
success: false,
|
|
128
|
-
error: "Invalid input for tool '".concat(this.toolProps.id, "': ").concat(errorDetails),
|
|
129
|
-
code: "VALIDATION_ERROR",
|
|
130
|
-
}];
|
|
131
|
-
}
|
|
132
|
-
validatedInput = input;
|
|
133
|
-
}
|
|
134
146
|
else {
|
|
135
147
|
// No schema available - skip validation
|
|
136
148
|
validatedInput = input;
|
|
@@ -147,9 +159,9 @@ var ToolExecutor = /** @class */ (function () {
|
|
|
147
159
|
}
|
|
148
160
|
// 3. Execute tool
|
|
149
161
|
toolLog.debug("Executing tool ".concat(this.toolProps.id));
|
|
150
|
-
|
|
162
|
+
_d.label = 3;
|
|
151
163
|
case 3:
|
|
152
|
-
|
|
164
|
+
_d.trys.push([3, 5, , 6]);
|
|
153
165
|
toolLog.trace(this.toolFn.name + " input:", validatedInput);
|
|
154
166
|
return [4 /*yield*/, this.toolFn({
|
|
155
167
|
input: validatedInput,
|
|
@@ -159,10 +171,10 @@ var ToolExecutor = /** @class */ (function () {
|
|
|
159
171
|
conversationCtx: conversationContext,
|
|
160
172
|
})];
|
|
161
173
|
case 4:
|
|
162
|
-
result =
|
|
174
|
+
result = _d.sent();
|
|
163
175
|
return [3 /*break*/, 6];
|
|
164
176
|
case 5:
|
|
165
|
-
err_1 =
|
|
177
|
+
err_1 = _d.sent();
|
|
166
178
|
toolLog.error("Tool ".concat(this.toolProps.id, " threw error:"), err_1.message);
|
|
167
179
|
return [2 /*return*/, {
|
|
168
180
|
success: false,
|
|
@@ -191,13 +203,12 @@ var ToolExecutor = /** @class */ (function () {
|
|
|
191
203
|
}];
|
|
192
204
|
}
|
|
193
205
|
}
|
|
194
|
-
else if (this.
|
|
195
|
-
// Priority 2: Use
|
|
206
|
+
else if (this.compiledOutputValidator) {
|
|
207
|
+
// Priority 2 & 3: Use pre-compiled JSON Schema validator (manual or auto-generated)
|
|
196
208
|
try {
|
|
197
|
-
|
|
198
|
-
valid = validate(result.data);
|
|
209
|
+
valid = this.compiledOutputValidator(result.data);
|
|
199
210
|
if (!valid) {
|
|
200
|
-
errorDetails = this.formatAjvErrors(
|
|
211
|
+
errorDetails = this.formatAjvErrors(this.compiledOutputValidator.errors || []);
|
|
201
212
|
toolLog.error("Tool ".concat(this.toolProps.id, " output validation failed:"), errorDetails);
|
|
202
213
|
return [2 /*return*/, {
|
|
203
214
|
success: false,
|
|
@@ -216,31 +227,6 @@ var ToolExecutor = /** @class */ (function () {
|
|
|
216
227
|
}];
|
|
217
228
|
}
|
|
218
229
|
}
|
|
219
|
-
else if ((_d = this.autoSchemas) === null || _d === void 0 ? void 0 : _d.outputSchema) {
|
|
220
|
-
// Priority 3: Use auto-generated JSON Schema validation
|
|
221
|
-
try {
|
|
222
|
-
validate = this.ajv.compile(this.autoSchemas.outputSchema);
|
|
223
|
-
valid = validate(result.data);
|
|
224
|
-
if (!valid) {
|
|
225
|
-
errorDetails = this.formatAjvErrors(validate.errors || []);
|
|
226
|
-
toolLog.error("Tool ".concat(this.toolProps.id, " output validation failed (auto-generated schema):"), errorDetails);
|
|
227
|
-
return [2 /*return*/, {
|
|
228
|
-
success: false,
|
|
229
|
-
error: "Invalid output from tool ".concat(this.toolProps.id, ": ").concat(errorDetails),
|
|
230
|
-
code: "OUTPUT_VALIDATION_ERROR",
|
|
231
|
-
}];
|
|
232
|
-
}
|
|
233
|
-
return [2 /*return*/, { success: true, data: result.data }];
|
|
234
|
-
}
|
|
235
|
-
catch (err) {
|
|
236
|
-
toolLog.error("Tool ".concat(this.toolProps.id, " output validation failed:"), err.message);
|
|
237
|
-
return [2 /*return*/, {
|
|
238
|
-
success: false,
|
|
239
|
-
error: "Invalid output from tool ".concat(this.toolProps.id, ": ").concat(err.message),
|
|
240
|
-
code: "OUTPUT_VALIDATION_ERROR",
|
|
241
|
-
}];
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
230
|
// No output validation - return result as-is
|
|
245
231
|
return [2 /*return*/, result];
|
|
246
232
|
}
|
|
@@ -65,4 +65,4 @@ import { InstructionsCallback, AgentExecuteContext } from "./FlinkAgent";
|
|
|
65
65
|
* @returns InstructionsCallback compatible with FlinkAgent.instructions property
|
|
66
66
|
*/
|
|
67
67
|
export declare function agentInstructions<Ctx extends FlinkContext = FlinkContext>(filePath: string, variables?: Record<string, any> | ((ctx: Ctx, agentContext: AgentExecuteContext) => Record<string, any> | Promise<Record<string, any>>)): InstructionsCallback<Ctx>;
|
|
68
|
-
export type { InstructionsCallback, AgentExecuteContext } from "./FlinkAgent";
|
|
68
|
+
export type { InstructionsCallback, AgentExecuteContext, InstructionsReturn } from "./FlinkAgent";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Return type for the FlinkAgent.instructions() method.
|
|
3
|
+
*
|
|
4
|
+
* Supported forms:
|
|
5
|
+
* - `string` — Plain text used as-is, OR a file path (see below) which is auto-loaded.
|
|
6
|
+
* - `{ file, params? }` — Explicitly load a file with optional Handlebars template params.
|
|
7
|
+
*
|
|
8
|
+
* **Path resolution** — all paths resolve relative to the **project root** (`process.cwd()`):
|
|
9
|
+
* - `"instructions/foo.md"` → `<project-root>/instructions/foo.md`
|
|
10
|
+
* - `"./instructions/foo.md"` → `<project-root>/instructions/foo.md`
|
|
11
|
+
* - `"/instructions/foo.md"` → `<project-root>/instructions/foo.md` (leading slash stripped)
|
|
12
|
+
*
|
|
13
|
+
* Auto-loaded string extensions: `.md`, `.txt`, `.yaml`, `.yml`, `.xml`, `.toml`, `.ini`, `.json`, `.html`
|
|
14
|
+
*
|
|
15
|
+
* @example Plain text
|
|
16
|
+
* instructions() {
|
|
17
|
+
* return "You are a helpful car assistant.";
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* @example Auto-load file (project-root-relative)
|
|
21
|
+
* instructions() {
|
|
22
|
+
* return "instructions/car-agent.md";
|
|
23
|
+
* }
|
|
24
|
+
*
|
|
25
|
+
* @example File with template params
|
|
26
|
+
* async instructions(_ctx, agentCtx) {
|
|
27
|
+
* return {
|
|
28
|
+
* file: "instructions/support.md",
|
|
29
|
+
* params: {
|
|
30
|
+
* customerTier: agentCtx.user?.tier || "standard",
|
|
31
|
+
* isBusinessHours: new Date().getHours() >= 9,
|
|
32
|
+
* },
|
|
33
|
+
* };
|
|
34
|
+
* }
|
|
35
|
+
*/
|
|
36
|
+
export type InstructionsReturn = string | {
|
|
37
|
+
file: string;
|
|
38
|
+
params?: Record<string, any>;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Resolve an InstructionsReturn value to a plain string.
|
|
42
|
+
* @internal Used by FlinkAgent.toAgentProps()
|
|
43
|
+
*/
|
|
44
|
+
export declare function resolveInstructionsReturn(result: InstructionsReturn, ctx: any, agentContext: any): Promise<string>;
|