@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 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
@@ -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
- valid = validateRes_1(JSON.parse(JSON.stringify(handlerRes.data)));
574
- if (!valid) {
575
- formattedErrors = (0, utils_1.formatValidationErrors)(validateRes_1.errors, handlerRes.data);
576
- FlinkLog_1.log.warn("[".concat(req.reqId, "] ").concat(methodAndRoute_1, ": Bad response\n").concat(formattedErrors));
577
- return [2 /*return*/, res.status(500).json({
578
- status: 500,
579
- error: {
580
- id: (0, uuid_1.v4)(),
581
- title: "Bad response",
582
- detail: formattedErrors,
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 job = new toad_scheduler_1.SimpleIntervalJob({
905
- milliseconds: (0, ms_1.default)(jobProps.afterDelay),
906
- runImmediately: false,
907
- }, task, {
908
- id: jobProps.id,
909
- preventOverrun: jobProps.singleton,
910
- });
911
- this_1.scheduler.addSimpleIntervalJob(job);
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
- toolExecutor = new ToolExecutor(toolFile.Tool, toolFile.default, this.ctx, schemas // Auto-generated schemas from manifest (resolved from definitions)
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, err_2;
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
- err_2 = _a.sent();
1130
- FlinkLog_1.log.error("Failed to connect to db: " + err_2);
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, err_3;
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
- err_3 = _a.sent();
1177
- FlinkLog_1.log.error("Failed to connect to db defined in plugin '".concat(plugin.id, "': ") + err_3);
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
  }
@@ -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(): FlinkContext;
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
- abstract instructions: AgentInstructions<Ctx>;
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 = (_b = input.userPermissions) !== null && _b !== void 0 ? _b : this._boundUserPermissions;
347
- var conversationContext = (_c = input.conversationContext) !== null && _c !== void 0 ? _c : this._boundConversationContext;
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: this.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, validate, valid, errorDetails, validate, valid, errorDetails, errorDetails, result, err_1, validatedData, validate, valid, errorDetails, validate, valid, errorDetails;
84
- var _a, _b, _c, _d;
85
- return __generator(this, function (_e) {
86
- switch (_e.label) {
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.getRequestPermissions)();
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 = _e.sent();
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
- _e.label = 2;
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.toolProps.inputJsonSchema) {
107
- validate = this.ajv.compile(this.toolProps.inputJsonSchema);
108
- valid = validate(input);
133
+ else if (this.compiledInputValidator) {
134
+ valid = this.compiledInputValidator(input);
109
135
  if (!valid) {
110
- errorDetails = this.formatAjvErrors(validate.errors || [], input);
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
- _e.label = 3;
162
+ _d.label = 3;
151
163
  case 3:
152
- _e.trys.push([3, 5, , 6]);
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 = _e.sent();
174
+ result = _d.sent();
163
175
  return [3 /*break*/, 6];
164
176
  case 5:
165
- err_1 = _e.sent();
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.toolProps.outputJsonSchema) {
195
- // Priority 2: Use manual JSON Schema validation
206
+ else if (this.compiledOutputValidator) {
207
+ // Priority 2 & 3: Use pre-compiled JSON Schema validator (manual or auto-generated)
196
208
  try {
197
- validate = this.ajv.compile(this.toolProps.outputJsonSchema);
198
- valid = validate(result.data);
209
+ valid = this.compiledOutputValidator(result.data);
199
210
  if (!valid) {
200
- errorDetails = this.formatAjvErrors(validate.errors || []);
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>;