@flink-app/flink 2.0.0-alpha.59 → 2.0.0-alpha.60

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,7 @@
1
1
  # @flink-app/flink
2
2
 
3
+ ## 2.0.0-alpha.60
4
+
3
5
  ## 2.0.0-alpha.59
4
6
 
5
7
  ### Minor Changes
package/cli/dev.ts CHANGED
@@ -6,6 +6,7 @@ import { existsSync } from "fs";
6
6
  import { parseArgs, hasFlag, getOption, normalizeEntry, printHelp, compile, forkApp } from "./cli-utils";
7
7
  import { FlinkLogFactory } from "../src/FlinkLogFactory";
8
8
  import { loadFlinkConfig } from "../src/utils/loadFlinkConfig";
9
+ import { loadEnvFiles } from "./loadEnvFiles";
9
10
 
10
11
  // Load config BEFORE creating logger
11
12
  const flinkConfig = loadFlinkConfig();
@@ -16,6 +17,19 @@ const logger = FlinkLogFactory.createLogger("flink.dev");
16
17
  module.exports = async function dev(args: string[]) {
17
18
  const parsed = parseArgs(args);
18
19
 
20
+ // Load .env files before anything else so all env vars are available at startup.
21
+ // NODE_ENV defaults to "development" when not explicitly set.
22
+ if (!process.env.NODE_ENV) {
23
+ process.env.NODE_ENV = "development";
24
+ }
25
+
26
+ const initLogger = FlinkLogFactory.createLogger("flink.init");
27
+ const { loaded, injected } = loadEnvFiles(parsed.dir, "development");
28
+
29
+ if (loaded.length > 0) {
30
+ initLogger.info(`Loaded env files: ${loaded.join(", ")} (${injected} vars injected)`);
31
+ }
32
+
19
33
  if (hasFlag(parsed, "help")) {
20
34
  printHelp({
21
35
  description:
@@ -0,0 +1,116 @@
1
+ import { readFileSync, existsSync } from "fs";
2
+ import { resolve, join } from "path";
3
+
4
+ /**
5
+ * Parse the contents of a .env file into a key/value map.
6
+ *
7
+ * Rules:
8
+ * - Empty lines and lines starting with # are skipped
9
+ * - "export FOO=bar" is treated the same as "FOO=bar"
10
+ * - Quoted values (single or double) are unquoted, and their content is used as-is
11
+ * - Unquoted values have inline comments stripped: FOO=bar # comment → "bar"
12
+ */
13
+ export function parseEnvContent(content: string): Record<string, string> {
14
+ const result: Record<string, string> = {};
15
+
16
+ for (const rawLine of content.split("\n")) {
17
+ let line = rawLine.trim();
18
+
19
+ // Skip empty lines and full-line comments
20
+ if (!line || line.startsWith("#")) continue;
21
+
22
+ // Strip "export " prefix
23
+ if (line.startsWith("export ")) {
24
+ line = line.slice(7).trim();
25
+ }
26
+
27
+ const eqIdx = line.indexOf("=");
28
+ if (eqIdx === -1) continue;
29
+
30
+ const key = line.slice(0, eqIdx).trim();
31
+ if (!key) continue;
32
+
33
+ let value = line.slice(eqIdx + 1);
34
+
35
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
36
+ // Quoted value — strip quotes, preserve content as-is
37
+ value = value.slice(1, -1);
38
+ } else {
39
+ // Unquoted — strip inline comments (" # ...") and trim
40
+ const commentIdx = value.search(/ #/);
41
+ if (commentIdx !== -1) {
42
+ value = value.slice(0, commentIdx);
43
+ }
44
+ value = value.trim();
45
+ }
46
+
47
+ result[key] = value;
48
+ }
49
+
50
+ return result;
51
+ }
52
+
53
+ function readEnvFile(filepath: string): Record<string, string> | null {
54
+ if (!existsSync(filepath)) return null;
55
+ try {
56
+ return parseEnvContent(readFileSync(filepath, "utf-8"));
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ export interface LoadEnvFilesResult {
63
+ /** Relative filenames that were found and loaded, in load order */
64
+ loaded: string[];
65
+ /** Number of variables injected into process.env (excludes already-set vars) */
66
+ injected: number;
67
+ }
68
+
69
+ /**
70
+ * Load .env files in Next.js-compatible order.
71
+ *
72
+ * Priority (highest wins):
73
+ * 1. process.env (already-set variables are never overridden)
74
+ * 2. .env.[environment].local
75
+ * 3. .env.local (skipped when environment === "test")
76
+ * 4. .env.[environment]
77
+ * 5. .env
78
+ *
79
+ * @param dir Root directory of the Flink app (where .env files live)
80
+ * @param environment "development" | "production" | "test"
81
+ */
82
+ export function loadEnvFiles(dir: string, environment: "development" | "production" | "test"): LoadEnvFilesResult {
83
+ const absoluteDir = resolve(dir);
84
+
85
+ // Files listed from lowest to highest priority so that Object.assign
86
+ // naturally lets later files override earlier ones.
87
+ const candidates: string[] = [
88
+ ".env",
89
+ `.env.${environment}`,
90
+ // .env.local is intentionally skipped in test (matches Next.js behaviour)
91
+ ...(environment !== "test" ? [".env.local"] : []),
92
+ `.env.${environment}.local`,
93
+ ];
94
+
95
+ const loaded: string[] = [];
96
+ const merged: Record<string, string> = {};
97
+
98
+ for (const filename of candidates) {
99
+ const parsed = readEnvFile(join(absoluteDir, filename));
100
+ if (parsed !== null) {
101
+ loaded.push(filename);
102
+ Object.assign(merged, parsed);
103
+ }
104
+ }
105
+
106
+ // Inject into process.env — existing values always win
107
+ let injected = 0;
108
+ for (const [key, value] of Object.entries(merged)) {
109
+ if (!(key in process.env)) {
110
+ process.env[key] = value;
111
+ injected++;
112
+ }
113
+ }
114
+
115
+ return { loaded, injected };
116
+ }
package/cli/run.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  import { parseArgs, hasFlag, getOption, normalizeEntry, printHelp, compile, forkApp } from "./cli-utils";
3
3
  import { FlinkLogFactory } from "../src/FlinkLogFactory";
4
4
  import { loadFlinkConfig } from "../src/utils/loadFlinkConfig";
5
+ import { loadEnvFiles } from "./loadEnvFiles";
5
6
 
6
7
  // Load config BEFORE creating logger
7
8
  const flinkConfig = loadFlinkConfig();
@@ -10,6 +11,19 @@ FlinkLogFactory.configure(flinkConfig?.logging);
10
11
  module.exports = async function run(args: string[]) {
11
12
  const parsed = parseArgs(args);
12
13
 
14
+ // Load .env files before anything else so all env vars are available at startup.
15
+ // NODE_ENV defaults to "production" when not explicitly set.
16
+ if (!process.env.NODE_ENV) {
17
+ process.env.NODE_ENV = "production";
18
+ }
19
+
20
+ const initLogger = FlinkLogFactory.createLogger("flink.init");
21
+ const { loaded, injected } = loadEnvFiles(parsed.dir, "production");
22
+
23
+ if (loaded.length > 0) {
24
+ initLogger.info(`Loaded env files: ${loaded.join(", ")} (${injected} vars injected)`);
25
+ }
26
+
13
27
  if (hasFlag(parsed, "help")) {
14
28
  printHelp({
15
29
  description: "Compiles and starts the application.",
package/dist/cli/dev.js CHANGED
@@ -47,18 +47,29 @@ var fs_1 = require("fs");
47
47
  var cli_utils_1 = require("./cli-utils");
48
48
  var FlinkLogFactory_1 = require("../src/FlinkLogFactory");
49
49
  var loadFlinkConfig_1 = require("../src/utils/loadFlinkConfig");
50
+ var loadEnvFiles_1 = require("./loadEnvFiles");
50
51
  // Load config BEFORE creating logger
51
52
  var flinkConfig = (0, loadFlinkConfig_1.loadFlinkConfig)();
52
53
  FlinkLogFactory_1.FlinkLogFactory.configure(flinkConfig === null || flinkConfig === void 0 ? void 0 : flinkConfig.logging);
53
54
  var logger = FlinkLogFactory_1.FlinkLogFactory.createLogger("flink.dev");
54
55
  module.exports = function dev(args) {
55
56
  return __awaiter(this, void 0, void 0, function () {
56
- var parsed, dir, entry, typecheck, noTypecheck, serverProcess, tscProcess, isRebuilding, pendingRebuild, watcher;
57
+ var parsed, initLogger, _a, loaded, injected, dir, entry, typecheck, noTypecheck, serverProcess, tscProcess, isRebuilding, pendingRebuild, watcher;
57
58
  var _this = this;
58
- return __generator(this, function (_a) {
59
- switch (_a.label) {
59
+ return __generator(this, function (_b) {
60
+ switch (_b.label) {
60
61
  case 0:
61
62
  parsed = (0, cli_utils_1.parseArgs)(args);
63
+ // Load .env files before anything else so all env vars are available at startup.
64
+ // NODE_ENV defaults to "development" when not explicitly set.
65
+ if (!process.env.NODE_ENV) {
66
+ process.env.NODE_ENV = "development";
67
+ }
68
+ initLogger = FlinkLogFactory_1.FlinkLogFactory.createLogger("flink.init");
69
+ _a = (0, loadEnvFiles_1.loadEnvFiles)(parsed.dir, "development"), loaded = _a.loaded, injected = _a.injected;
70
+ if (loaded.length > 0) {
71
+ initLogger.info("Loaded env files: ".concat(loaded.join(", "), " (").concat(injected, " vars injected)"));
72
+ }
62
73
  if ((0, cli_utils_1.hasFlag)(parsed, "help")) {
63
74
  (0, cli_utils_1.printHelp)({
64
75
  description: "Starts development mode with watch and auto-reload.\n Fast rebuilds with swc (skips type checking by default).\n Runs background tsc --watch for async type error reporting.",
@@ -81,7 +92,7 @@ module.exports = function dev(args) {
81
92
  console.log("\uD83D\uDCE6 Initial build ".concat(typecheck ? "(with type checking)" : "(fast mode, skipping type check)", "..."));
82
93
  return [4 /*yield*/, (0, cli_utils_1.compile)({ dir: dir, entry: entry, typeCheck: typecheck })];
83
94
  case 1:
84
- _a.sent();
95
+ _b.sent();
85
96
  serverProcess = null;
86
97
  tscProcess = null;
87
98
  isRebuilding = false;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Parse the contents of a .env file into a key/value map.
3
+ *
4
+ * Rules:
5
+ * - Empty lines and lines starting with # are skipped
6
+ * - "export FOO=bar" is treated the same as "FOO=bar"
7
+ * - Quoted values (single or double) are unquoted, and their content is used as-is
8
+ * - Unquoted values have inline comments stripped: FOO=bar # comment → "bar"
9
+ */
10
+ export declare function parseEnvContent(content: string): Record<string, string>;
11
+ export interface LoadEnvFilesResult {
12
+ /** Relative filenames that were found and loaded, in load order */
13
+ loaded: string[];
14
+ /** Number of variables injected into process.env (excludes already-set vars) */
15
+ injected: number;
16
+ }
17
+ /**
18
+ * Load .env files in Next.js-compatible order.
19
+ *
20
+ * Priority (highest wins):
21
+ * 1. process.env (already-set variables are never overridden)
22
+ * 2. .env.[environment].local
23
+ * 3. .env.local (skipped when environment === "test")
24
+ * 4. .env.[environment]
25
+ * 5. .env
26
+ *
27
+ * @param dir Root directory of the Flink app (where .env files live)
28
+ * @param environment "development" | "production" | "test"
29
+ */
30
+ export declare function loadEnvFiles(dir: string, environment: "development" | "production" | "test"): LoadEnvFilesResult;
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
3
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
4
+ if (ar || !(i in from)) {
5
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
6
+ ar[i] = from[i];
7
+ }
8
+ }
9
+ return to.concat(ar || Array.prototype.slice.call(from));
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.parseEnvContent = parseEnvContent;
13
+ exports.loadEnvFiles = loadEnvFiles;
14
+ var fs_1 = require("fs");
15
+ var path_1 = require("path");
16
+ /**
17
+ * Parse the contents of a .env file into a key/value map.
18
+ *
19
+ * Rules:
20
+ * - Empty lines and lines starting with # are skipped
21
+ * - "export FOO=bar" is treated the same as "FOO=bar"
22
+ * - Quoted values (single or double) are unquoted, and their content is used as-is
23
+ * - Unquoted values have inline comments stripped: FOO=bar # comment → "bar"
24
+ */
25
+ function parseEnvContent(content) {
26
+ var result = {};
27
+ for (var _i = 0, _a = content.split("\n"); _i < _a.length; _i++) {
28
+ var rawLine = _a[_i];
29
+ var line = rawLine.trim();
30
+ // Skip empty lines and full-line comments
31
+ if (!line || line.startsWith("#"))
32
+ continue;
33
+ // Strip "export " prefix
34
+ if (line.startsWith("export ")) {
35
+ line = line.slice(7).trim();
36
+ }
37
+ var eqIdx = line.indexOf("=");
38
+ if (eqIdx === -1)
39
+ continue;
40
+ var key = line.slice(0, eqIdx).trim();
41
+ if (!key)
42
+ continue;
43
+ var value = line.slice(eqIdx + 1);
44
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
45
+ // Quoted value — strip quotes, preserve content as-is
46
+ value = value.slice(1, -1);
47
+ }
48
+ else {
49
+ // Unquoted — strip inline comments (" # ...") and trim
50
+ var commentIdx = value.search(/ #/);
51
+ if (commentIdx !== -1) {
52
+ value = value.slice(0, commentIdx);
53
+ }
54
+ value = value.trim();
55
+ }
56
+ result[key] = value;
57
+ }
58
+ return result;
59
+ }
60
+ function readEnvFile(filepath) {
61
+ if (!(0, fs_1.existsSync)(filepath))
62
+ return null;
63
+ try {
64
+ return parseEnvContent((0, fs_1.readFileSync)(filepath, "utf-8"));
65
+ }
66
+ catch (_a) {
67
+ return null;
68
+ }
69
+ }
70
+ /**
71
+ * Load .env files in Next.js-compatible order.
72
+ *
73
+ * Priority (highest wins):
74
+ * 1. process.env (already-set variables are never overridden)
75
+ * 2. .env.[environment].local
76
+ * 3. .env.local (skipped when environment === "test")
77
+ * 4. .env.[environment]
78
+ * 5. .env
79
+ *
80
+ * @param dir Root directory of the Flink app (where .env files live)
81
+ * @param environment "development" | "production" | "test"
82
+ */
83
+ function loadEnvFiles(dir, environment) {
84
+ var absoluteDir = (0, path_1.resolve)(dir);
85
+ // Files listed from lowest to highest priority so that Object.assign
86
+ // naturally lets later files override earlier ones.
87
+ var candidates = __spreadArray(__spreadArray([
88
+ ".env",
89
+ ".env.".concat(environment)
90
+ ], (environment !== "test" ? [".env.local"] : []), true), [
91
+ ".env.".concat(environment, ".local"),
92
+ ], false);
93
+ var loaded = [];
94
+ var merged = {};
95
+ for (var _i = 0, candidates_1 = candidates; _i < candidates_1.length; _i++) {
96
+ var filename = candidates_1[_i];
97
+ var parsed = readEnvFile((0, path_1.join)(absoluteDir, filename));
98
+ if (parsed !== null) {
99
+ loaded.push(filename);
100
+ Object.assign(merged, parsed);
101
+ }
102
+ }
103
+ // Inject into process.env — existing values always win
104
+ var injected = 0;
105
+ for (var _a = 0, _b = Object.entries(merged); _a < _b.length; _a++) {
106
+ var _c = _b[_a], key = _c[0], value = _c[1];
107
+ if (!(key in process.env)) {
108
+ process.env[key] = value;
109
+ injected++;
110
+ }
111
+ }
112
+ return { loaded: loaded, injected: injected };
113
+ }
package/dist/cli/run.js CHANGED
@@ -40,16 +40,27 @@ Object.defineProperty(exports, "__esModule", { value: true });
40
40
  var cli_utils_1 = require("./cli-utils");
41
41
  var FlinkLogFactory_1 = require("../src/FlinkLogFactory");
42
42
  var loadFlinkConfig_1 = require("../src/utils/loadFlinkConfig");
43
+ var loadEnvFiles_1 = require("./loadEnvFiles");
43
44
  // Load config BEFORE creating logger
44
45
  var flinkConfig = (0, loadFlinkConfig_1.loadFlinkConfig)();
45
46
  FlinkLogFactory_1.FlinkLogFactory.configure(flinkConfig === null || flinkConfig === void 0 ? void 0 : flinkConfig.logging);
46
47
  module.exports = function run(args) {
47
48
  return __awaiter(this, void 0, void 0, function () {
48
- var parsed, entry;
49
- return __generator(this, function (_a) {
50
- switch (_a.label) {
49
+ var parsed, initLogger, _a, loaded, injected, entry;
50
+ return __generator(this, function (_b) {
51
+ switch (_b.label) {
51
52
  case 0:
52
53
  parsed = (0, cli_utils_1.parseArgs)(args);
54
+ // Load .env files before anything else so all env vars are available at startup.
55
+ // NODE_ENV defaults to "production" when not explicitly set.
56
+ if (!process.env.NODE_ENV) {
57
+ process.env.NODE_ENV = "production";
58
+ }
59
+ initLogger = FlinkLogFactory_1.FlinkLogFactory.createLogger("flink.init");
60
+ _a = (0, loadEnvFiles_1.loadEnvFiles)(parsed.dir, "production"), loaded = _a.loaded, injected = _a.injected;
61
+ if (loaded.length > 0) {
62
+ initLogger.info("Loaded env files: ".concat(loaded.join(", "), " (").concat(injected, " vars injected)"));
63
+ }
53
64
  if ((0, cli_utils_1.hasFlag)(parsed, "help")) {
54
65
  (0, cli_utils_1.printHelp)({
55
66
  description: "Compiles and starts the application.",
@@ -80,7 +91,7 @@ module.exports = function run(args) {
80
91
  typeCheck: true,
81
92
  })];
82
93
  case 1:
83
- _a.sent();
94
+ _b.sent();
84
95
  (0, cli_utils_1.forkApp)({
85
96
  dir: parsed.dir,
86
97
  args: args,
@@ -132,7 +132,7 @@ var AgentRunner = /** @class */ (function () {
132
132
  return [3 /*break*/, 6];
133
133
  case 5:
134
134
  err_1 = _o.sent();
135
- throw new Error("Failed to resolve dynamic instructions for agent ".concat(this.agentName, ": ").concat(err_1.message));
135
+ throw new Error("Failed to resolve instructions for agent ".concat(this.agentName, ": ").concat(err_1.message));
136
136
  case 6:
137
137
  if (input.history && input.history.length > 0) {
138
138
  // Start with history
@@ -481,8 +481,9 @@ export interface AgentExecuteInput<ConversationCtx = any> {
481
481
  */
482
482
  conversationContext?: ConversationCtx;
483
483
  /**
484
- * Previous conversation messages for context
485
- * Agent can load this in beforeRun hook based on conversationId
484
+ * Previous conversation messages for context, in chronological order (oldest first).
485
+ * The new `message` will be appended after these history messages.
486
+ * Agent can load this in beforeRun hook based on conversationId.
486
487
  */
487
488
  history?: Message[];
488
489
  /**
@@ -84,19 +84,25 @@ var fileCache = new Map();
84
84
  /**
85
85
  * Get caller's file location using stack trace API
86
86
  * This enables agent-relative path resolution for ./ and ../ prefixes
87
+ *
88
+ * Walks the stack to find the first frame outside this file,
89
+ * which is the agent class that called agentInstructions().
87
90
  */
88
91
  function getCallerFilePath() {
92
+ var _a, _b;
89
93
  var originalPrepareStackTrace = Error.prepareStackTrace;
90
94
  Error.prepareStackTrace = function (_, stack) { return stack; };
91
95
  var stack = new Error().stack;
92
96
  Error.prepareStackTrace = originalPrepareStackTrace;
93
- // Stack: [0] getCallerFilePath, [1] agentInstructions, [2] agent class
94
- var callerFrame = stack[2];
95
- var fileName = callerFrame === null || callerFrame === void 0 ? void 0 : callerFrame.getFileName();
96
- if (!fileName) {
97
- throw new Error("Could not determine caller file location for agent-relative path");
97
+ // Find first frame outside this file (skips getCallerFilePath, resolveFilePath, agentInstructions)
98
+ var thisFile = (_a = stack[0]) === null || _a === void 0 ? void 0 : _a.getFileName();
99
+ for (var i = 1; i < stack.length; i++) {
100
+ var fileName = (_b = stack[i]) === null || _b === void 0 ? void 0 : _b.getFileName();
101
+ if (fileName && fileName !== thisFile) {
102
+ return fileName;
103
+ }
98
104
  }
99
- return fileName;
105
+ throw new Error("Could not determine caller file location for agent-relative path");
100
106
  }
101
107
  /**
102
108
  * Resolve file path based on prefix:
@@ -104,14 +110,33 @@ function getCallerFilePath() {
104
110
  * - otherwise = project root-relative
105
111
  */
106
112
  function resolveFilePath(filePath) {
113
+ // Always try project root-relative first (most intuitive for users)
114
+ var cwdResolved = path.join(process.cwd(), filePath);
115
+ if (fs.existsSync(cwdResolved)) {
116
+ return cwdResolved;
117
+ }
107
118
  if (filePath.startsWith("./") || filePath.startsWith("../")) {
108
119
  // Agent-relative: get caller's file location via stack trace
109
120
  var callerFile = getCallerFilePath();
110
- return path.resolve(path.dirname(callerFile), filePath);
121
+ var resolved = path.resolve(path.dirname(callerFile), filePath);
122
+ if (fs.existsSync(resolved)) {
123
+ return resolved;
124
+ }
125
+ // At runtime, callerFile points to compiled JS in dist/.
126
+ // Non-JS assets (e.g. .md instruction files) live in the source tree,
127
+ // so try the equivalent source path with dist/ removed.
128
+ var distIndex = resolved.lastIndexOf(path.sep + "dist" + path.sep);
129
+ if (distIndex !== -1) {
130
+ var sourcePath = resolved.substring(0, distIndex) + resolved.substring(distIndex + 5); // remove "/dist"
131
+ if (fs.existsSync(sourcePath)) {
132
+ return sourcePath;
133
+ }
134
+ }
135
+ return resolved;
111
136
  }
112
137
  else {
113
138
  // Project root-relative
114
- return path.join(process.cwd(), filePath);
139
+ return cwdResolved;
115
140
  }
116
141
  }
117
142
  /**
@@ -125,12 +150,13 @@ function loadFile(resolvedPath, originalPath) {
125
150
  // Check cache
126
151
  var cached = fileCache.get(resolvedPath);
127
152
  if (cached && cached.mtime === mtime) {
128
- return cached.content;
153
+ return cached;
129
154
  }
130
- // Read file and update cache
155
+ // Read file and update cache (clear compiled template on file change)
131
156
  var content = fs.readFileSync(resolvedPath, "utf-8");
132
- fileCache.set(resolvedPath, { content: content, mtime: mtime });
133
- return content;
157
+ var entry = { content: content, mtime: mtime };
158
+ fileCache.set(resolvedPath, entry);
159
+ return entry;
134
160
  }
135
161
  catch (err) {
136
162
  if (err.code === "ENOENT") {
@@ -140,12 +166,22 @@ function loadFile(resolvedPath, originalPath) {
140
166
  }
141
167
  }
142
168
  /**
143
- * Render template using Handlebars
169
+ * Render template using Handlebars with compiled template caching.
170
+ * Skips Handlebars entirely when content has no template expressions.
144
171
  */
145
- function renderTemplate(content, data, filePath) {
172
+ function renderTemplate(cached, data, filePath) {
173
+ if (cached.hasTemplateExpressions === false) {
174
+ return cached.content;
175
+ }
146
176
  try {
147
- var template = handlebars_1.default.compile(content);
148
- return template(data);
177
+ if (!cached.compiledTemplate) {
178
+ cached.hasTemplateExpressions = /\{\{/.test(cached.content);
179
+ if (!cached.hasTemplateExpressions) {
180
+ return cached.content;
181
+ }
182
+ cached.compiledTemplate = handlebars_1.default.compile(cached.content);
183
+ }
184
+ return cached.compiledTemplate(data);
149
185
  }
150
186
  catch (err) {
151
187
  throw new Error("Failed to render template in ".concat(filePath, ": ").concat(err.message));
@@ -221,11 +257,15 @@ function agentInstructions(filePath, variables) {
221
257
  var resolvedPath = resolveFilePath(filePath);
222
258
  // Return callback that will be invoked by FlinkAgent
223
259
  return function (ctx, agentContext) { return __awaiter(_this, void 0, void 0, function () {
224
- var content, vars, templateData;
260
+ var cached, vars, templateData;
225
261
  return __generator(this, function (_a) {
226
262
  switch (_a.label) {
227
263
  case 0:
228
- content = loadFile(resolvedPath, filePath);
264
+ cached = loadFile(resolvedPath, filePath);
265
+ // Skip template processing entirely when no expressions exist
266
+ if (cached.hasTemplateExpressions === false) {
267
+ return [2 /*return*/, cached.content];
268
+ }
229
269
  vars = {};
230
270
  if (!variables) return [3 /*break*/, 3];
231
271
  if (!(typeof variables === "function")) return [3 /*break*/, 2];
@@ -238,8 +278,8 @@ function agentInstructions(filePath, variables) {
238
278
  _a.label = 3;
239
279
  case 3:
240
280
  templateData = __assign(__assign({}, vars), { ctx: ctx, agentContext: agentContext, user: agentContext.user });
241
- // Render template
242
- return [2 /*return*/, renderTemplate(content, templateData, resolvedPath)];
281
+ // Render template (compiles and caches Handlebars template on first call)
282
+ return [2 /*return*/, renderTemplate(cached, templateData, resolvedPath)];
243
283
  }
244
284
  });
245
285
  }); };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flink-app/flink",
3
- "version": "2.0.0-alpha.59",
3
+ "version": "2.0.0-alpha.60",
4
4
  "description": "Typescript only framework for creating REST-like APIs on top of Express and mongodb",
5
5
  "types": "dist/src/index.d.ts",
6
6
  "main": "dist/src/index.js",
@@ -56,7 +56,7 @@
56
56
  "ts-morph": "24.0.0",
57
57
  "uuid": "^8.3.2",
58
58
  "zod": "^4.3.6",
59
- "@flink-app/ts-source-to-json-schema": "^0.1.5"
59
+ "@flink-app/ts-source-to-json-schema": "^0.1.6"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@swc/core": "^1.15.17",
@@ -993,7 +993,7 @@ describe("AgentRunner", () => {
993
993
  }
994
994
  fail("Expected error to be thrown");
995
995
  } catch (err: any) {
996
- expect(err.message).toContain("Failed to resolve dynamic instructions for agent test_agent");
996
+ expect(err.message).toContain("Failed to resolve instructions for agent test_agent");
997
997
  expect(err.message).toContain("Database connection failed");
998
998
  }
999
999
  });
@@ -504,7 +504,7 @@ describe("FlinkAgent", () => {
504
504
  (agent as any).ctx = mockCtx;
505
505
  agent.__init(new Map([["default", mockLLMAdapter]]), {});
506
506
 
507
- await expectAsync(agent.query("Hello")).toBeRejectedWithError(/Failed to resolve dynamic instructions/);
507
+ await expectAsync(agent.query("Hello")).toBeRejectedWithError(/Failed to resolve instructions/);
508
508
  });
509
509
  });
510
510
 
@@ -60,7 +60,7 @@ export class AgentRunner {
60
60
  resolvedInstructions =
61
61
  typeof this.agentProps.instructions === "function" ? await this.agentProps.instructions(this.ctx, execContext) : this.agentProps.instructions;
62
62
  } catch (err: any) {
63
- throw new Error(`Failed to resolve dynamic instructions for agent ${this.agentName}: ${err.message}`);
63
+ throw new Error(`Failed to resolve instructions for agent ${this.agentName}: ${err.message}`);
64
64
  }
65
65
 
66
66
  // Initialize messages from history + new input
@@ -841,8 +841,9 @@ export interface AgentExecuteInput<ConversationCtx = any> {
841
841
  conversationContext?: ConversationCtx;
842
842
 
843
843
  /**
844
- * Previous conversation messages for context
845
- * Agent can load this in beforeRun hook based on conversationId
844
+ * Previous conversation messages for context, in chronological order (oldest first).
845
+ * The new `message` will be appended after these history messages.
846
+ * Agent can load this in beforeRun hook based on conversationId.
846
847
  */
847
848
  history?: Message[];
848
849
 
@@ -10,6 +10,8 @@ import { InstructionsCallback, AgentExecuteContext } from "./FlinkAgent";
10
10
  interface FileCacheEntry {
11
11
  content: string;
12
12
  mtime: number;
13
+ compiledTemplate?: Handlebars.TemplateDelegate;
14
+ hasTemplateExpressions?: boolean;
13
15
  }
14
16
 
15
17
  /**
@@ -20,6 +22,9 @@ const fileCache = new Map<string, FileCacheEntry>();
20
22
  /**
21
23
  * Get caller's file location using stack trace API
22
24
  * This enables agent-relative path resolution for ./ and ../ prefixes
25
+ *
26
+ * Walks the stack to find the first frame outside this file,
27
+ * which is the agent class that called agentInstructions().
23
28
  */
24
29
  function getCallerFilePath(): string {
25
30
  const originalPrepareStackTrace = Error.prepareStackTrace;
@@ -27,15 +32,16 @@ function getCallerFilePath(): string {
27
32
  const stack = new Error().stack as unknown as NodeJS.CallSite[];
28
33
  Error.prepareStackTrace = originalPrepareStackTrace;
29
34
 
30
- // Stack: [0] getCallerFilePath, [1] agentInstructions, [2] agent class
31
- const callerFrame = stack[2];
32
- const fileName = callerFrame?.getFileName();
33
-
34
- if (!fileName) {
35
- throw new Error("Could not determine caller file location for agent-relative path");
35
+ // Find first frame outside this file (skips getCallerFilePath, resolveFilePath, agentInstructions)
36
+ const thisFile = stack[0]?.getFileName();
37
+ for (let i = 1; i < stack.length; i++) {
38
+ const fileName = stack[i]?.getFileName();
39
+ if (fileName && fileName !== thisFile) {
40
+ return fileName;
41
+ }
36
42
  }
37
43
 
38
- return fileName;
44
+ throw new Error("Could not determine caller file location for agent-relative path");
39
45
  }
40
46
 
41
47
  /**
@@ -44,13 +50,36 @@ function getCallerFilePath(): string {
44
50
  * - otherwise = project root-relative
45
51
  */
46
52
  function resolveFilePath(filePath: string): string {
53
+ // Always try project root-relative first (most intuitive for users)
54
+ const cwdResolved = path.join(process.cwd(), filePath);
55
+ if (fs.existsSync(cwdResolved)) {
56
+ return cwdResolved;
57
+ }
58
+
47
59
  if (filePath.startsWith("./") || filePath.startsWith("../")) {
48
60
  // Agent-relative: get caller's file location via stack trace
49
61
  const callerFile = getCallerFilePath();
50
- return path.resolve(path.dirname(callerFile), filePath);
62
+ const resolved = path.resolve(path.dirname(callerFile), filePath);
63
+
64
+ if (fs.existsSync(resolved)) {
65
+ return resolved;
66
+ }
67
+
68
+ // At runtime, callerFile points to compiled JS in dist/.
69
+ // Non-JS assets (e.g. .md instruction files) live in the source tree,
70
+ // so try the equivalent source path with dist/ removed.
71
+ const distIndex = resolved.lastIndexOf(path.sep + "dist" + path.sep);
72
+ if (distIndex !== -1) {
73
+ const sourcePath = resolved.substring(0, distIndex) + resolved.substring(distIndex + 5); // remove "/dist"
74
+ if (fs.existsSync(sourcePath)) {
75
+ return sourcePath;
76
+ }
77
+ }
78
+
79
+ return resolved;
51
80
  } else {
52
81
  // Project root-relative
53
- return path.join(process.cwd(), filePath);
82
+ return cwdResolved;
54
83
  }
55
84
  }
56
85
 
@@ -58,7 +87,7 @@ function resolveFilePath(filePath: string): string {
58
87
  * Load file contents with smart caching
59
88
  * Cache is invalidated when file modification time changes
60
89
  */
61
- function loadFile(resolvedPath: string, originalPath: string): string {
90
+ function loadFile(resolvedPath: string, originalPath: string): FileCacheEntry {
62
91
  try {
63
92
  const stats = fs.statSync(resolvedPath);
64
93
  const mtime = stats.mtimeMs;
@@ -66,14 +95,15 @@ function loadFile(resolvedPath: string, originalPath: string): string {
66
95
  // Check cache
67
96
  const cached = fileCache.get(resolvedPath);
68
97
  if (cached && cached.mtime === mtime) {
69
- return cached.content;
98
+ return cached;
70
99
  }
71
100
 
72
- // Read file and update cache
101
+ // Read file and update cache (clear compiled template on file change)
73
102
  const content = fs.readFileSync(resolvedPath, "utf-8");
74
- fileCache.set(resolvedPath, { content, mtime });
103
+ const entry: FileCacheEntry = { content, mtime };
104
+ fileCache.set(resolvedPath, entry);
75
105
 
76
- return content;
106
+ return entry;
77
107
  } catch (err: any) {
78
108
  if (err.code === "ENOENT") {
79
109
  throw new Error(`Agent instructions file not found: ${resolvedPath} (from: ${originalPath})`);
@@ -83,12 +113,23 @@ function loadFile(resolvedPath: string, originalPath: string): string {
83
113
  }
84
114
 
85
115
  /**
86
- * Render template using Handlebars
116
+ * Render template using Handlebars with compiled template caching.
117
+ * Skips Handlebars entirely when content has no template expressions.
87
118
  */
88
- function renderTemplate(content: string, data: any, filePath: string): string {
119
+ function renderTemplate(cached: FileCacheEntry, data: any, filePath: string): string {
120
+ if (cached.hasTemplateExpressions === false) {
121
+ return cached.content;
122
+ }
123
+
89
124
  try {
90
- const template = Handlebars.compile(content);
91
- return template(data);
125
+ if (!cached.compiledTemplate) {
126
+ cached.hasTemplateExpressions = /\{\{/.test(cached.content);
127
+ if (!cached.hasTemplateExpressions) {
128
+ return cached.content;
129
+ }
130
+ cached.compiledTemplate = Handlebars.compile(cached.content);
131
+ }
132
+ return cached.compiledTemplate(data);
92
133
  } catch (err: any) {
93
134
  throw new Error(`Failed to render template in ${filePath}: ${err.message}`);
94
135
  }
@@ -170,7 +211,12 @@ export function agentInstructions<Ctx extends FlinkContext = FlinkContext>(
170
211
  // Return callback that will be invoked by FlinkAgent
171
212
  return async (ctx: Ctx, agentContext: AgentExecuteContext): Promise<string> => {
172
213
  // Load file (uses cache if available and mtime unchanged)
173
- const content = loadFile(resolvedPath, filePath);
214
+ const cached = loadFile(resolvedPath, filePath);
215
+
216
+ // Skip template processing entirely when no expressions exist
217
+ if (cached.hasTemplateExpressions === false) {
218
+ return cached.content;
219
+ }
174
220
 
175
221
  // Resolve variables (static or callback)
176
222
  let vars: Record<string, any> = {};
@@ -190,8 +236,8 @@ export function agentInstructions<Ctx extends FlinkContext = FlinkContext>(
190
236
  user: agentContext.user,
191
237
  };
192
238
 
193
- // Render template
194
- return renderTemplate(content, templateData, resolvedPath);
239
+ // Render template (compiles and caches Handlebars template on first call)
240
+ return renderTemplate(cached, templateData, resolvedPath);
195
241
  };
196
242
  }
197
243