@flink-app/flink 2.0.0-alpha.65 → 2.0.0-alpha.67
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 +22 -0
- package/cli/dev.ts +19 -2
- package/dist/cli/dev.js +28 -10
- package/dist/src/ai/AgentRunner.js +47 -63
- package/dist/src/ai/FlinkAgent.d.ts +0 -12
- package/dist/src/ai/FlinkAgent.js +0 -1
- package/dist/src/ai/ToolExecutor.d.ts +11 -1
- package/dist/src/ai/ToolExecutor.js +64 -1
- package/dist/src/ai/instructionFileLoader.js +28 -5
- package/package.json +1 -1
- package/spec/ToolExecutor.spec.ts +113 -0
- package/src/ai/AgentRunner.ts +49 -65
- package/src/ai/FlinkAgent.ts +0 -13
- package/src/ai/ToolExecutor.ts +69 -2
- package/src/ai/instructionFileLoader.ts +29 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @flink-app/flink
|
|
2
2
|
|
|
3
|
+
## 2.0.0-alpha.67
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 418cb54: feat(instructions): add `{{import "..."}}` directive for composable markdown instruction files
|
|
8
|
+
|
|
9
|
+
Markdown instruction files can now import other markdown files using `{{import "./path/to/file.md"}}`. Imports resolve relative to the containing file, support recursive chaining, and detect circular imports with a clear error message. Handlebars template variables continue to work normally across all imported content.
|
|
10
|
+
|
|
11
|
+
- 70d574c: refactor(agent): remove debug flag in favour of log levels
|
|
12
|
+
|
|
13
|
+
The `debug` property on `FlinkAgent` and `FlinkAgentProps` is removed. Verbose agent logging (LLM requests, responses, tool calls, compaction) is now always emitted at the `debug` log level and can be enabled via the standard Flink log level configuration (e.g. `LOG_LEVEL=debug` or per-logger overrides). This removes a redundant knob that duplicated what log levels already provide.
|
|
14
|
+
|
|
15
|
+
## 2.0.0-alpha.66
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- 5593d26: fix(tools): resolve cross-schema \$ref into \$defs for LLM providers
|
|
20
|
+
|
|
21
|
+
Tools using auto-generated schemas with TypeScript type references (e.g. a shared Canvas.ElementInput type) produced schemas with \$ref values like `"Canvas.ElementInput"`. AJV handled these fine via its schema registry, but OpenAI rejects them with "reference can only point to definitions defined at the top level of the schema".
|
|
22
|
+
|
|
23
|
+
ToolExecutor now resolves all cross-schema refs into a self-contained \$defs block, rewriting \$ref values to the standard #/\$defs/... format before returning the schema to LLM adapters. The openai-adapter sanitizer also now recurses into \$defs entries.
|
|
24
|
+
|
|
3
25
|
## 2.0.0-alpha.65
|
|
4
26
|
|
|
5
27
|
## 2.0.0-alpha.64
|
package/cli/dev.ts
CHANGED
|
@@ -91,9 +91,9 @@ module.exports = async function dev(args: string[]) {
|
|
|
91
91
|
try {
|
|
92
92
|
await compile({ dir, entry, typeCheck: typecheck });
|
|
93
93
|
|
|
94
|
-
// Restart server
|
|
94
|
+
// Restart server — wait for old process to fully exit before binding port again
|
|
95
95
|
if (serverProcess) {
|
|
96
|
-
serverProcess
|
|
96
|
+
await killAndWait(serverProcess);
|
|
97
97
|
serverProcess = null;
|
|
98
98
|
}
|
|
99
99
|
serverProcess = startServer(dir);
|
|
@@ -135,6 +135,23 @@ module.exports = async function dev(args: string[]) {
|
|
|
135
135
|
});
|
|
136
136
|
};
|
|
137
137
|
|
|
138
|
+
function killAndWait(proc: ChildProcess): Promise<void> {
|
|
139
|
+
return new Promise((resolve) => {
|
|
140
|
+
if (proc.exitCode !== null) {
|
|
141
|
+
// Already exited
|
|
142
|
+
resolve();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
proc.once("exit", () => resolve());
|
|
146
|
+
proc.kill();
|
|
147
|
+
// Safety timeout: if the process doesn't exit within 5s, force-kill it
|
|
148
|
+
const timeout = setTimeout(() => {
|
|
149
|
+
proc.kill("SIGKILL");
|
|
150
|
+
}, 5000);
|
|
151
|
+
proc.once("exit", () => clearTimeout(timeout));
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
138
155
|
function startServer(dir: string): ChildProcess {
|
|
139
156
|
console.log("🚀 Starting server...");
|
|
140
157
|
|
package/dist/cli/dev.js
CHANGED
|
@@ -123,24 +123,26 @@ module.exports = function dev(args) {
|
|
|
123
123
|
rebuildStart = Date.now();
|
|
124
124
|
_a.label = 1;
|
|
125
125
|
case 1:
|
|
126
|
-
_a.trys.push([1,
|
|
126
|
+
_a.trys.push([1, 5, , 6]);
|
|
127
127
|
return [4 /*yield*/, (0, cli_utils_1.compile)({ dir: dir, entry: entry, typeCheck: typecheck })];
|
|
128
128
|
case 2:
|
|
129
129
|
_a.sent();
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
130
|
+
if (!serverProcess) return [3 /*break*/, 4];
|
|
131
|
+
return [4 /*yield*/, killAndWait(serverProcess)];
|
|
132
|
+
case 3:
|
|
133
|
+
_a.sent();
|
|
134
|
+
serverProcess = null;
|
|
135
|
+
_a.label = 4;
|
|
136
|
+
case 4:
|
|
135
137
|
serverProcess = startServer(dir);
|
|
136
138
|
rebuildTime = Date.now() - rebuildStart;
|
|
137
139
|
logger.info("\u2705 Rebuild complete in ".concat(rebuildTime, "ms\n"));
|
|
138
|
-
return [3 /*break*/,
|
|
139
|
-
case
|
|
140
|
+
return [3 /*break*/, 6];
|
|
141
|
+
case 5:
|
|
140
142
|
error_1 = _a.sent();
|
|
141
143
|
logger.error("\u274C Rebuild failed: ".concat(error_1.message));
|
|
142
|
-
return [3 /*break*/,
|
|
143
|
-
case
|
|
144
|
+
return [3 /*break*/, 6];
|
|
145
|
+
case 6:
|
|
144
146
|
isRebuilding = false;
|
|
145
147
|
// If another change happened during rebuild, trigger again
|
|
146
148
|
if (pendingRebuild) {
|
|
@@ -175,6 +177,22 @@ module.exports = function dev(args) {
|
|
|
175
177
|
});
|
|
176
178
|
});
|
|
177
179
|
};
|
|
180
|
+
function killAndWait(proc) {
|
|
181
|
+
return new Promise(function (resolve) {
|
|
182
|
+
if (proc.exitCode !== null) {
|
|
183
|
+
// Already exited
|
|
184
|
+
resolve();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
proc.once("exit", function () { return resolve(); });
|
|
188
|
+
proc.kill();
|
|
189
|
+
// Safety timeout: if the process doesn't exit within 5s, force-kill it
|
|
190
|
+
var timeout = setTimeout(function () {
|
|
191
|
+
proc.kill("SIGKILL");
|
|
192
|
+
}, 5000);
|
|
193
|
+
proc.once("exit", function () { return clearTimeout(timeout); });
|
|
194
|
+
});
|
|
195
|
+
}
|
|
178
196
|
function startServer(dir) {
|
|
179
197
|
console.log("🚀 Starting server...");
|
|
180
198
|
var fork = require("child_process").fork;
|
|
@@ -196,18 +196,15 @@ var AgentRunner = /** @class */ (function () {
|
|
|
196
196
|
messages = compactedMessages;
|
|
197
197
|
// Log compaction for debugging
|
|
198
198
|
FlinkLog_1.log.debug("[Agent:".concat(this.agentName, "] Step ").concat(step, ": Compacted ").concat(beforeCount, " messages \u2192 ").concat(messages.length));
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}); }),
|
|
209
|
-
});
|
|
210
|
-
}
|
|
199
|
+
FlinkLog_1.log.debug("[Agent:".concat(this.agentName, "] Compacted messages:"), {
|
|
200
|
+
messageCount: messages.length,
|
|
201
|
+
messages: messages.map(function (m) { return ({
|
|
202
|
+
role: m.role,
|
|
203
|
+
contentPreview: typeof m.content === "string"
|
|
204
|
+
? m.content.substring(0, 100) + (m.content.length > 100 ? "..." : "")
|
|
205
|
+
: "".concat(m.content.length, " blocks"),
|
|
206
|
+
}); }),
|
|
207
|
+
});
|
|
211
208
|
_o.label = 14;
|
|
212
209
|
case 14: return [3 /*break*/, 16];
|
|
213
210
|
case 15:
|
|
@@ -216,24 +213,20 @@ var AgentRunner = /** @class */ (function () {
|
|
|
216
213
|
FlinkLog_1.log.error("[Agent:".concat(this.agentName, "] Context compaction failed:"), error_1.message);
|
|
217
214
|
return [3 /*break*/, 16];
|
|
218
215
|
case 16:
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
maxTokens: this.maxTokens,
|
|
234
|
-
temperature: this.temperature,
|
|
235
|
-
});
|
|
236
|
-
}
|
|
216
|
+
FlinkLog_1.log.debug("[Agent:".concat(this.agentName, "] Step ").concat(step, "/").concat(maxSteps, " - Calling LLM with:"), {
|
|
217
|
+
instructionsType: typeof this.agentProps.instructions === "function" ? "dynamic-callback" : "static",
|
|
218
|
+
messageCount: messages.length,
|
|
219
|
+
messages: messages.map(function (m) { return ({
|
|
220
|
+
role: m.role,
|
|
221
|
+
contentPreview: typeof m.content === "string"
|
|
222
|
+
? m.content.substring(0, 100) + (m.content.length > 100 ? "..." : "")
|
|
223
|
+
: "".concat(m.content.length, " blocks"),
|
|
224
|
+
}); }),
|
|
225
|
+
toolCount: availableTools.length,
|
|
226
|
+
tools: availableTools.map(function (t) { return t.name; }),
|
|
227
|
+
maxTokens: this.maxTokens,
|
|
228
|
+
temperature: this.temperature,
|
|
229
|
+
});
|
|
237
230
|
llmStream = this.llmAdapter.stream({
|
|
238
231
|
instructions: resolvedInstructions,
|
|
239
232
|
messages: messages,
|
|
@@ -322,21 +315,18 @@ var AgentRunner = /** @class */ (function () {
|
|
|
322
315
|
usage: usage,
|
|
323
316
|
stopReason: stopReason,
|
|
324
317
|
};
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
usage: llmResponse.usage,
|
|
338
|
-
});
|
|
339
|
-
}
|
|
318
|
+
FlinkLog_1.log.debug("[Agent:".concat(this.agentName, "] Step ").concat(step, " - LLM Response:"), {
|
|
319
|
+
textLength: ((_l = llmResponse.textContent) === null || _l === void 0 ? void 0 : _l.length) || 0,
|
|
320
|
+
textPreview: ((_m = llmResponse.textContent) === null || _m === void 0 ? void 0 : _m.substring(0, 200)) + (llmResponse.textContent && llmResponse.textContent.length > 200 ? "..." : ""),
|
|
321
|
+
toolCallsCount: llmResponse.toolCalls.length,
|
|
322
|
+
toolCalls: llmResponse.toolCalls.map(function (tc) { return ({
|
|
323
|
+
name: tc.name,
|
|
324
|
+
inputKeys: Object.keys(tc.input),
|
|
325
|
+
input: tc.input,
|
|
326
|
+
}); }),
|
|
327
|
+
stopReason: llmResponse.stopReason,
|
|
328
|
+
usage: llmResponse.usage,
|
|
329
|
+
});
|
|
340
330
|
// Extract text response
|
|
341
331
|
if (llmResponse.textContent) {
|
|
342
332
|
finalMessage = llmResponse.textContent;
|
|
@@ -379,13 +369,10 @@ var AgentRunner = /** @class */ (function () {
|
|
|
379
369
|
toolExecutor = this.tools.get(toolCall.name);
|
|
380
370
|
toolOutput = void 0;
|
|
381
371
|
toolError = void 0;
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
inputSize: JSON.stringify(toolCall.input).length,
|
|
387
|
-
});
|
|
388
|
-
}
|
|
372
|
+
FlinkLog_1.log.debug("[Agent:".concat(this.agentName, "] Executing tool '").concat(toolCall.name, "':"), {
|
|
373
|
+
input: toolCall.input,
|
|
374
|
+
inputSize: JSON.stringify(toolCall.input).length,
|
|
375
|
+
});
|
|
389
376
|
_o.label = 40;
|
|
390
377
|
case 40:
|
|
391
378
|
_o.trys.push([40, 46, , 49]);
|
|
@@ -442,17 +429,14 @@ var AgentRunner = /** @class */ (function () {
|
|
|
442
429
|
output: toolResult.success ? toolResult.data : null,
|
|
443
430
|
error: toolResult.success ? undefined : toolResult.error,
|
|
444
431
|
});
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
code: toolResult.code,
|
|
454
|
-
});
|
|
455
|
-
}
|
|
432
|
+
FlinkLog_1.log.debug("[Agent:".concat(this.agentName, "] Tool '").concat(toolCall.name, "' ").concat(toolResult.success ? "succeeded" : "failed", ":"), {
|
|
433
|
+
success: toolResult.success,
|
|
434
|
+
outputSize: toolResult.success ? JSON.stringify(toolResult.data).length : 0,
|
|
435
|
+
outputPreview: toolResult.success
|
|
436
|
+
? JSON.stringify(toolResult.data).substring(0, 200) + (JSON.stringify(toolResult.data).length > 200 ? "..." : "")
|
|
437
|
+
: toolResult.error,
|
|
438
|
+
code: toolResult.code,
|
|
439
|
+
});
|
|
456
440
|
if (!toolResult.success) {
|
|
457
441
|
FlinkLog_1.log.warn("Tool ".concat(toolCall.name, " returned error:"), toolResult.error);
|
|
458
442
|
}
|
|
@@ -116,17 +116,6 @@ export interface FlinkAgentProps<Ctx extends FlinkContext> {
|
|
|
116
116
|
timeoutMs?: number;
|
|
117
117
|
};
|
|
118
118
|
permissions?: string | string[] | ((user?: any) => boolean);
|
|
119
|
-
/**
|
|
120
|
-
* Enable verbose debug logging for this agent
|
|
121
|
-
* When true, logs detailed information about:
|
|
122
|
-
* - Full LLM requests (messages, tools, parameters)
|
|
123
|
-
* - Full LLM responses (text, tool calls)
|
|
124
|
-
* - Tool execution details (input, output, errors)
|
|
125
|
-
* - Conversation state changes
|
|
126
|
-
*
|
|
127
|
-
* Useful for debugging tool calling issues and understanding agent behavior
|
|
128
|
-
*/
|
|
129
|
-
debug?: boolean;
|
|
130
119
|
/**
|
|
131
120
|
* Callback to determine if history compaction is needed
|
|
132
121
|
* Called before each LLM call in the agentic loop
|
|
@@ -297,7 +286,6 @@ export declare abstract class FlinkAgent<Ctx extends FlinkContext, ConversationC
|
|
|
297
286
|
timeoutMs?: number;
|
|
298
287
|
};
|
|
299
288
|
permissions?: string | string[] | ((user?: any) => boolean);
|
|
300
|
-
debug?: boolean;
|
|
301
289
|
protected shouldCompact?: ShouldCompactCallback;
|
|
302
290
|
protected compactHistory?: CompactHistoryCallback;
|
|
303
291
|
/**
|
|
@@ -647,7 +647,6 @@ var FlinkAgent = /** @class */ (function () {
|
|
|
647
647
|
model: this.model,
|
|
648
648
|
limits: this.limits,
|
|
649
649
|
permissions: this.permissions,
|
|
650
|
-
debug: this.debug,
|
|
651
650
|
// Pass context compaction callbacks
|
|
652
651
|
shouldCompact: (_a = this.shouldCompact) === null || _a === void 0 ? void 0 : _a.bind(this),
|
|
653
652
|
compactHistory: (_b = this.compactHistory) === null || _b === void 0 ? void 0 : _b.bind(this),
|
|
@@ -6,6 +6,7 @@ export declare class ToolExecutor<Ctx extends FlinkContext> {
|
|
|
6
6
|
private toolFn;
|
|
7
7
|
private ctx;
|
|
8
8
|
private autoSchemas?;
|
|
9
|
+
private allSchemas?;
|
|
9
10
|
private ajv;
|
|
10
11
|
private compiledInputValidator?;
|
|
11
12
|
private compiledOutputValidator?;
|
|
@@ -14,7 +15,7 @@ export declare class ToolExecutor<Ctx extends FlinkContext> {
|
|
|
14
15
|
outputSchema?: any;
|
|
15
16
|
inputTypeHint?: "void" | "any" | "named";
|
|
16
17
|
outputTypeHint?: "void" | "any" | "named";
|
|
17
|
-
} | undefined, allSchemas?: Record<string, any>);
|
|
18
|
+
} | undefined, allSchemas?: Record<string, any> | undefined);
|
|
18
19
|
/**
|
|
19
20
|
* Execute the tool with input
|
|
20
21
|
* @param input - Tool input data
|
|
@@ -26,6 +27,15 @@ export declare class ToolExecutor<Ctx extends FlinkContext> {
|
|
|
26
27
|
conversationContext?: any;
|
|
27
28
|
}): Promise<ToolResult<any>>;
|
|
28
29
|
getToolSchema(): FlinkToolSchema;
|
|
30
|
+
/**
|
|
31
|
+
* Resolve cross-schema $ref values into $defs so the schema is self-contained.
|
|
32
|
+
*
|
|
33
|
+
* Flink's schema manifest stores schemas as separate documents with IDs like
|
|
34
|
+
* "Canvas.ElementInput". These are valid for AJV (which uses a schema registry),
|
|
35
|
+
* but LLM providers like OpenAI require a single self-contained schema where all
|
|
36
|
+
* $ref values point to #/$defs/... entries at the top level.
|
|
37
|
+
*/
|
|
38
|
+
private resolveSchemaRefs;
|
|
29
39
|
/**
|
|
30
40
|
* Get tool result for AI consumption
|
|
31
41
|
* Formats ToolResult into string for AI context
|
|
@@ -51,6 +51,7 @@ var ToolExecutor = /** @class */ (function () {
|
|
|
51
51
|
this.toolFn = toolFn;
|
|
52
52
|
this.ctx = ctx;
|
|
53
53
|
this.autoSchemas = autoSchemas;
|
|
54
|
+
this.allSchemas = allSchemas;
|
|
54
55
|
this.ajv = new ajv_1.default({ allErrors: true });
|
|
55
56
|
// Pre-populate AJV with all schemas so $ref references resolve across schema boundaries
|
|
56
57
|
if (allSchemas) {
|
|
@@ -263,7 +264,7 @@ var ToolExecutor = /** @class */ (function () {
|
|
|
263
264
|
return {
|
|
264
265
|
name: this.toolProps.id,
|
|
265
266
|
description: this.toolProps.description,
|
|
266
|
-
inputSchema: this.autoSchemas.inputSchema,
|
|
267
|
+
inputSchema: this.resolveSchemaRefs(this.autoSchemas.inputSchema),
|
|
267
268
|
};
|
|
268
269
|
}
|
|
269
270
|
// No schema provided - return schema based on type hint
|
|
@@ -304,6 +305,68 @@ var ToolExecutor = /** @class */ (function () {
|
|
|
304
305
|
};
|
|
305
306
|
}
|
|
306
307
|
};
|
|
308
|
+
/**
|
|
309
|
+
* Resolve cross-schema $ref values into $defs so the schema is self-contained.
|
|
310
|
+
*
|
|
311
|
+
* Flink's schema manifest stores schemas as separate documents with IDs like
|
|
312
|
+
* "Canvas.ElementInput". These are valid for AJV (which uses a schema registry),
|
|
313
|
+
* but LLM providers like OpenAI require a single self-contained schema where all
|
|
314
|
+
* $ref values point to #/$defs/... entries at the top level.
|
|
315
|
+
*/
|
|
316
|
+
ToolExecutor.prototype.resolveSchemaRefs = function (schema) {
|
|
317
|
+
var _this = this;
|
|
318
|
+
if (!this.allSchemas)
|
|
319
|
+
return schema;
|
|
320
|
+
var defs = {};
|
|
321
|
+
var collectRefs = function (node, visited) {
|
|
322
|
+
if (!node || typeof node !== "object")
|
|
323
|
+
return;
|
|
324
|
+
if (node.$ref && typeof node.$ref === "string" && !node.$ref.startsWith("#")) {
|
|
325
|
+
var refId = node.$ref;
|
|
326
|
+
if (!visited.has(refId) && _this.allSchemas[refId]) {
|
|
327
|
+
visited.add(refId);
|
|
328
|
+
// Clone and strip top-level JSON Schema meta fields not valid inside $defs
|
|
329
|
+
var def = JSON.parse(JSON.stringify(_this.allSchemas[refId]));
|
|
330
|
+
delete def.$id;
|
|
331
|
+
delete def.$schema;
|
|
332
|
+
defs[refId] = def;
|
|
333
|
+
// Recurse into the referenced schema to collect its deps
|
|
334
|
+
collectRefs(def, visited);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
for (var _i = 0, _a = Object.values(node); _i < _a.length; _i++) {
|
|
338
|
+
var value = _a[_i];
|
|
339
|
+
collectRefs(value, visited);
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
var visited = new Set();
|
|
343
|
+
collectRefs(schema, visited);
|
|
344
|
+
if (Object.keys(defs).length === 0)
|
|
345
|
+
return schema;
|
|
346
|
+
// Deep clone and rewrite all non-standard $ref values to #/$defs/<id>
|
|
347
|
+
var resolved = JSON.parse(JSON.stringify(schema));
|
|
348
|
+
delete resolved.$id;
|
|
349
|
+
delete resolved.$schema;
|
|
350
|
+
var rewriteRefs = function (node) {
|
|
351
|
+
if (!node || typeof node !== "object")
|
|
352
|
+
return;
|
|
353
|
+
if (node.$ref && typeof node.$ref === "string" && !node.$ref.startsWith("#")) {
|
|
354
|
+
node.$ref = "#/$defs/".concat(node.$ref);
|
|
355
|
+
}
|
|
356
|
+
for (var _i = 0, _a = Object.values(node); _i < _a.length; _i++) {
|
|
357
|
+
var value = _a[_i];
|
|
358
|
+
rewriteRefs(value);
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
rewriteRefs(resolved);
|
|
362
|
+
// Also rewrite refs inside the collected defs
|
|
363
|
+
for (var _i = 0, _a = Object.values(defs); _i < _a.length; _i++) {
|
|
364
|
+
var def = _a[_i];
|
|
365
|
+
rewriteRefs(def);
|
|
366
|
+
}
|
|
367
|
+
resolved.$defs = defs;
|
|
368
|
+
return resolved;
|
|
369
|
+
};
|
|
307
370
|
/**
|
|
308
371
|
* Get tool result for AI consumption
|
|
309
372
|
* Formats ToolResult into string for AI context
|
|
@@ -78,6 +78,28 @@ var fs = __importStar(require("fs"));
|
|
|
78
78
|
var path = __importStar(require("path"));
|
|
79
79
|
var handlebars_1 = __importDefault(require("handlebars"));
|
|
80
80
|
var fileCache = new Map();
|
|
81
|
+
var IMPORT_REGEX = /\{\{\s*import\s+["']([^"']+)['"]\s*\}\}/g;
|
|
82
|
+
function resolveImports(content, currentDir, visited) {
|
|
83
|
+
if (visited === void 0) { visited = new Set(); }
|
|
84
|
+
return content.replace(IMPORT_REGEX, function (match, importPath) {
|
|
85
|
+
var resolved = path.resolve(currentDir, importPath);
|
|
86
|
+
if (visited.has(resolved)) {
|
|
87
|
+
throw new Error("Circular import detected: ".concat(resolved));
|
|
88
|
+
}
|
|
89
|
+
var nextVisited = new Set(visited);
|
|
90
|
+
nextVisited.add(resolved);
|
|
91
|
+
try {
|
|
92
|
+
var importedContent = fs.readFileSync(resolved, "utf-8");
|
|
93
|
+
return resolveImports(importedContent, path.dirname(resolved), nextVisited);
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
if (err.code === "ENOENT") {
|
|
97
|
+
throw new Error("Imported markdown file not found: ".concat(resolved, " (imported from: ").concat(currentDir, ")"));
|
|
98
|
+
}
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
81
103
|
/**
|
|
82
104
|
* Resolve a file path relative to project root (process.cwd()).
|
|
83
105
|
* Leading `./` and `/` are normalised away — all paths are treated as project-root-relative.
|
|
@@ -97,7 +119,8 @@ function loadFile(filePath) {
|
|
|
97
119
|
return cached;
|
|
98
120
|
}
|
|
99
121
|
var content = fs.readFileSync(resolvedPath, "utf-8");
|
|
100
|
-
var
|
|
122
|
+
var resolvedContent = resolveImports(content, path.dirname(resolvedPath));
|
|
123
|
+
var entry = { content: content, resolvedContent: resolvedContent, mtime: mtime };
|
|
101
124
|
fileCache.set(resolvedPath, entry);
|
|
102
125
|
return entry;
|
|
103
126
|
}
|
|
@@ -110,14 +133,14 @@ function loadFile(filePath) {
|
|
|
110
133
|
}
|
|
111
134
|
function renderTemplate(entry, data) {
|
|
112
135
|
if (entry.hasTemplateExpressions === false) {
|
|
113
|
-
return entry.
|
|
136
|
+
return entry.resolvedContent;
|
|
114
137
|
}
|
|
115
138
|
if (!entry.compiledTemplate) {
|
|
116
|
-
entry.hasTemplateExpressions = /\{\{/.test(entry.
|
|
139
|
+
entry.hasTemplateExpressions = /\{\{/.test(entry.resolvedContent);
|
|
117
140
|
if (!entry.hasTemplateExpressions) {
|
|
118
|
-
return entry.
|
|
141
|
+
return entry.resolvedContent;
|
|
119
142
|
}
|
|
120
|
-
entry.compiledTemplate = handlebars_1.default.compile(entry.
|
|
143
|
+
entry.compiledTemplate = handlebars_1.default.compile(entry.resolvedContent);
|
|
121
144
|
}
|
|
122
145
|
return entry.compiledTemplate(data);
|
|
123
146
|
}
|
package/package.json
CHANGED
|
@@ -276,6 +276,119 @@ describe("ToolExecutor", () => {
|
|
|
276
276
|
expect(schema.description).toBe("Get weather for a city");
|
|
277
277
|
expect(schema.inputSchema).toBeDefined();
|
|
278
278
|
});
|
|
279
|
+
|
|
280
|
+
describe("cross-schema $ref resolution", () => {
|
|
281
|
+
const allSchemas = {
|
|
282
|
+
"MyTool.Input": {
|
|
283
|
+
$id: "MyTool.Input",
|
|
284
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
285
|
+
type: "object",
|
|
286
|
+
properties: {
|
|
287
|
+
id: { type: "string" },
|
|
288
|
+
item: { $ref: "Shared.Item" },
|
|
289
|
+
},
|
|
290
|
+
required: ["id", "item"],
|
|
291
|
+
},
|
|
292
|
+
"Shared.Item": {
|
|
293
|
+
$id: "Shared.Item",
|
|
294
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
295
|
+
type: "object",
|
|
296
|
+
properties: {
|
|
297
|
+
name: { type: "string" },
|
|
298
|
+
tag: { $ref: "Shared.Tag" },
|
|
299
|
+
},
|
|
300
|
+
required: ["name"],
|
|
301
|
+
},
|
|
302
|
+
"Shared.Tag": {
|
|
303
|
+
$id: "Shared.Tag",
|
|
304
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
305
|
+
type: "string",
|
|
306
|
+
enum: ["foo", "bar"],
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
it("should resolve cross-schema $refs into $defs", () => {
|
|
311
|
+
const toolProps: FlinkToolProps = { id: "my-tool", description: "Test" };
|
|
312
|
+
const toolFn: FlinkTool<any> = async () => ({ success: true, data: {} });
|
|
313
|
+
const executor = new ToolExecutor(
|
|
314
|
+
toolProps,
|
|
315
|
+
toolFn,
|
|
316
|
+
mockCtx,
|
|
317
|
+
{ inputSchema: allSchemas["MyTool.Input"], inputTypeHint: "named" },
|
|
318
|
+
allSchemas
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
const { inputSchema } = executor.getToolSchema();
|
|
322
|
+
|
|
323
|
+
// $ref values should use JSON pointer format
|
|
324
|
+
expect(inputSchema.properties.item.$ref).toBe("#/$defs/Shared.Item");
|
|
325
|
+
|
|
326
|
+
// All transitively referenced schemas should be in $defs
|
|
327
|
+
expect(inputSchema.$defs).toBeDefined();
|
|
328
|
+
expect(inputSchema.$defs["Shared.Item"]).toBeDefined();
|
|
329
|
+
expect(inputSchema.$defs["Shared.Tag"]).toBeDefined();
|
|
330
|
+
|
|
331
|
+
// $refs inside $defs should also be rewritten
|
|
332
|
+
expect(inputSchema.$defs["Shared.Item"].properties.tag.$ref).toBe("#/$defs/Shared.Tag");
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("should strip $id and $schema from inlined $defs", () => {
|
|
336
|
+
const toolProps: FlinkToolProps = { id: "my-tool", description: "Test" };
|
|
337
|
+
const toolFn: FlinkTool<any> = async () => ({ success: true, data: {} });
|
|
338
|
+
const executor = new ToolExecutor(
|
|
339
|
+
toolProps,
|
|
340
|
+
toolFn,
|
|
341
|
+
mockCtx,
|
|
342
|
+
{ inputSchema: allSchemas["MyTool.Input"], inputTypeHint: "named" },
|
|
343
|
+
allSchemas
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
const { inputSchema } = executor.getToolSchema();
|
|
347
|
+
|
|
348
|
+
expect(inputSchema.$defs["Shared.Item"].$id).toBeUndefined();
|
|
349
|
+
expect(inputSchema.$defs["Shared.Item"].$schema).toBeUndefined();
|
|
350
|
+
expect(inputSchema.$defs["Shared.Tag"].$id).toBeUndefined();
|
|
351
|
+
expect(inputSchema.$defs["Shared.Tag"].$schema).toBeUndefined();
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("should strip $id and $schema from root schema", () => {
|
|
355
|
+
const toolProps: FlinkToolProps = { id: "my-tool", description: "Test" };
|
|
356
|
+
const toolFn: FlinkTool<any> = async () => ({ success: true, data: {} });
|
|
357
|
+
const executor = new ToolExecutor(
|
|
358
|
+
toolProps,
|
|
359
|
+
toolFn,
|
|
360
|
+
mockCtx,
|
|
361
|
+
{ inputSchema: allSchemas["MyTool.Input"], inputTypeHint: "named" },
|
|
362
|
+
allSchemas
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
const { inputSchema } = executor.getToolSchema();
|
|
366
|
+
|
|
367
|
+
expect(inputSchema.$id).toBeUndefined();
|
|
368
|
+
expect(inputSchema.$schema).toBeUndefined();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("should return schema unchanged when no cross-schema refs exist", () => {
|
|
372
|
+
const simpleSchema = {
|
|
373
|
+
$id: "Simple.Input",
|
|
374
|
+
type: "object",
|
|
375
|
+
properties: { name: { type: "string" } },
|
|
376
|
+
};
|
|
377
|
+
const toolProps: FlinkToolProps = { id: "simple-tool", description: "Test" };
|
|
378
|
+
const toolFn: FlinkTool<any> = async () => ({ success: true, data: {} });
|
|
379
|
+
const executor = new ToolExecutor(
|
|
380
|
+
toolProps,
|
|
381
|
+
toolFn,
|
|
382
|
+
mockCtx,
|
|
383
|
+
{ inputSchema: simpleSchema, inputTypeHint: "named" },
|
|
384
|
+
allSchemas
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
const { inputSchema } = executor.getToolSchema();
|
|
388
|
+
|
|
389
|
+
expect(inputSchema.$defs).toBeUndefined();
|
|
390
|
+
});
|
|
391
|
+
});
|
|
279
392
|
});
|
|
280
393
|
|
|
281
394
|
describe("Result formatting for AI", () => {
|
package/src/ai/AgentRunner.ts
CHANGED
|
@@ -127,19 +127,16 @@ export class AgentRunner {
|
|
|
127
127
|
// Log compaction for debugging
|
|
128
128
|
log.debug(`[Agent:${this.agentName}] Step ${step}: Compacted ${beforeCount} messages → ${messages.length}`);
|
|
129
129
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
})),
|
|
141
|
-
});
|
|
142
|
-
}
|
|
130
|
+
log.debug(`[Agent:${this.agentName}] Compacted messages:`, {
|
|
131
|
+
messageCount: messages.length,
|
|
132
|
+
messages: messages.map((m) => ({
|
|
133
|
+
role: m.role,
|
|
134
|
+
contentPreview:
|
|
135
|
+
typeof m.content === "string"
|
|
136
|
+
? m.content.substring(0, 100) + (m.content.length > 100 ? "..." : "")
|
|
137
|
+
: `${(m.content as any[]).length} blocks`,
|
|
138
|
+
})),
|
|
139
|
+
});
|
|
143
140
|
}
|
|
144
141
|
} catch (error: any) {
|
|
145
142
|
// Log error but don't fail execution - compaction is optional optimization
|
|
@@ -147,25 +144,21 @@ export class AgentRunner {
|
|
|
147
144
|
}
|
|
148
145
|
}
|
|
149
146
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
maxTokens: this.maxTokens,
|
|
166
|
-
temperature: this.temperature,
|
|
167
|
-
});
|
|
168
|
-
}
|
|
147
|
+
log.debug(`[Agent:${this.agentName}] Step ${step}/${maxSteps} - Calling LLM with:`, {
|
|
148
|
+
instructionsType: typeof this.agentProps.instructions === "function" ? "dynamic-callback" : "static",
|
|
149
|
+
messageCount: messages.length,
|
|
150
|
+
messages: messages.map((m) => ({
|
|
151
|
+
role: m.role,
|
|
152
|
+
contentPreview:
|
|
153
|
+
typeof m.content === "string"
|
|
154
|
+
? m.content.substring(0, 100) + (m.content.length > 100 ? "..." : "")
|
|
155
|
+
: `${(m.content as any[]).length} blocks`,
|
|
156
|
+
})),
|
|
157
|
+
toolCount: availableTools.length,
|
|
158
|
+
tools: availableTools.map((t) => t.name),
|
|
159
|
+
maxTokens: this.maxTokens,
|
|
160
|
+
temperature: this.temperature,
|
|
161
|
+
});
|
|
169
162
|
|
|
170
163
|
// Call AI model via adapter using streaming
|
|
171
164
|
const llmStream = this.llmAdapter.stream({
|
|
@@ -225,21 +218,18 @@ export class AgentRunner {
|
|
|
225
218
|
stopReason,
|
|
226
219
|
};
|
|
227
220
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
usage: llmResponse.usage,
|
|
241
|
-
});
|
|
242
|
-
}
|
|
221
|
+
log.debug(`[Agent:${this.agentName}] Step ${step} - LLM Response:`, {
|
|
222
|
+
textLength: llmResponse.textContent?.length || 0,
|
|
223
|
+
textPreview: llmResponse.textContent?.substring(0, 200) + (llmResponse.textContent && llmResponse.textContent.length > 200 ? "..." : ""),
|
|
224
|
+
toolCallsCount: llmResponse.toolCalls.length,
|
|
225
|
+
toolCalls: llmResponse.toolCalls.map((tc) => ({
|
|
226
|
+
name: tc.name,
|
|
227
|
+
inputKeys: Object.keys(tc.input),
|
|
228
|
+
input: tc.input,
|
|
229
|
+
})),
|
|
230
|
+
stopReason: llmResponse.stopReason,
|
|
231
|
+
usage: llmResponse.usage,
|
|
232
|
+
});
|
|
243
233
|
|
|
244
234
|
// Extract text response
|
|
245
235
|
if (llmResponse.textContent) {
|
|
@@ -293,13 +283,10 @@ export class AgentRunner {
|
|
|
293
283
|
let toolOutput: any;
|
|
294
284
|
let toolError: string | undefined;
|
|
295
285
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
inputSize: JSON.stringify(toolCall.input).length,
|
|
301
|
-
});
|
|
302
|
-
}
|
|
286
|
+
log.debug(`[Agent:${this.agentName}] Executing tool '${toolCall.name}':`, {
|
|
287
|
+
input: toolCall.input,
|
|
288
|
+
inputSize: JSON.stringify(toolCall.input).length,
|
|
289
|
+
});
|
|
303
290
|
|
|
304
291
|
try {
|
|
305
292
|
if (!toolExecutor) {
|
|
@@ -352,17 +339,14 @@ export class AgentRunner {
|
|
|
352
339
|
error: toolResult.success ? undefined : toolResult.error,
|
|
353
340
|
});
|
|
354
341
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
code: (toolResult as any).code,
|
|
364
|
-
});
|
|
365
|
-
}
|
|
342
|
+
log.debug(`[Agent:${this.agentName}] Tool '${toolCall.name}' ${toolResult.success ? "succeeded" : "failed"}:`, {
|
|
343
|
+
success: toolResult.success,
|
|
344
|
+
outputSize: toolResult.success ? JSON.stringify(toolResult.data).length : 0,
|
|
345
|
+
outputPreview: toolResult.success
|
|
346
|
+
? JSON.stringify(toolResult.data).substring(0, 200) + (JSON.stringify(toolResult.data).length > 200 ? "..." : "")
|
|
347
|
+
: toolResult.error,
|
|
348
|
+
code: (toolResult as any).code,
|
|
349
|
+
});
|
|
366
350
|
|
|
367
351
|
if (!toolResult.success) {
|
|
368
352
|
log.warn(`Tool ${toolCall.name} returned error:`, toolResult.error);
|
package/src/ai/FlinkAgent.ts
CHANGED
|
@@ -130,17 +130,6 @@ export interface FlinkAgentProps<Ctx extends FlinkContext> {
|
|
|
130
130
|
timeoutMs?: number; // Phase 2: Not yet implemented
|
|
131
131
|
};
|
|
132
132
|
permissions?: string | string[] | ((user?: any) => boolean);
|
|
133
|
-
/**
|
|
134
|
-
* Enable verbose debug logging for this agent
|
|
135
|
-
* When true, logs detailed information about:
|
|
136
|
-
* - Full LLM requests (messages, tools, parameters)
|
|
137
|
-
* - Full LLM responses (text, tool calls)
|
|
138
|
-
* - Tool execution details (input, output, errors)
|
|
139
|
-
* - Conversation state changes
|
|
140
|
-
*
|
|
141
|
-
* Useful for debugging tool calling issues and understanding agent behavior
|
|
142
|
-
*/
|
|
143
|
-
debug?: boolean;
|
|
144
133
|
|
|
145
134
|
/**
|
|
146
135
|
* Callback to determine if history compaction is needed
|
|
@@ -319,7 +308,6 @@ export abstract class FlinkAgent<Ctx extends FlinkContext, ConversationCtx = any
|
|
|
319
308
|
};
|
|
320
309
|
limits?: { maxSteps?: number; timeoutMs?: number };
|
|
321
310
|
permissions?: string | string[] | ((user?: any) => boolean);
|
|
322
|
-
debug?: boolean; // Enable verbose debug logging
|
|
323
311
|
protected shouldCompact?: ShouldCompactCallback;
|
|
324
312
|
protected compactHistory?: CompactHistoryCallback;
|
|
325
313
|
|
|
@@ -755,7 +743,6 @@ export abstract class FlinkAgent<Ctx extends FlinkContext, ConversationCtx = any
|
|
|
755
743
|
model: this.model,
|
|
756
744
|
limits: this.limits,
|
|
757
745
|
permissions: this.permissions,
|
|
758
|
-
debug: this.debug,
|
|
759
746
|
// Pass context compaction callbacks
|
|
760
747
|
shouldCompact: this.shouldCompact?.bind(this),
|
|
761
748
|
compactHistory: this.compactHistory?.bind(this),
|
package/src/ai/ToolExecutor.ts
CHANGED
|
@@ -23,7 +23,7 @@ export class ToolExecutor<Ctx extends FlinkContext> {
|
|
|
23
23
|
inputTypeHint?: 'void' | 'any' | 'named';
|
|
24
24
|
outputTypeHint?: 'void' | 'any' | 'named';
|
|
25
25
|
},
|
|
26
|
-
allSchemas?: Record<string, any>
|
|
26
|
+
private allSchemas?: Record<string, any>
|
|
27
27
|
) {
|
|
28
28
|
// Pre-populate AJV with all schemas so $ref references resolve across schema boundaries
|
|
29
29
|
if (allSchemas) {
|
|
@@ -238,7 +238,7 @@ export class ToolExecutor<Ctx extends FlinkContext> {
|
|
|
238
238
|
return {
|
|
239
239
|
name: this.toolProps.id,
|
|
240
240
|
description: this.toolProps.description,
|
|
241
|
-
inputSchema: this.autoSchemas.inputSchema,
|
|
241
|
+
inputSchema: this.resolveSchemaRefs(this.autoSchemas.inputSchema),
|
|
242
242
|
};
|
|
243
243
|
}
|
|
244
244
|
|
|
@@ -280,6 +280,73 @@ export class ToolExecutor<Ctx extends FlinkContext> {
|
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Resolve cross-schema $ref values into $defs so the schema is self-contained.
|
|
285
|
+
*
|
|
286
|
+
* Flink's schema manifest stores schemas as separate documents with IDs like
|
|
287
|
+
* "Canvas.ElementInput". These are valid for AJV (which uses a schema registry),
|
|
288
|
+
* but LLM providers like OpenAI require a single self-contained schema where all
|
|
289
|
+
* $ref values point to #/$defs/... entries at the top level.
|
|
290
|
+
*/
|
|
291
|
+
private resolveSchemaRefs(schema: any): any {
|
|
292
|
+
if (!this.allSchemas) return schema;
|
|
293
|
+
|
|
294
|
+
const defs: Record<string, any> = {};
|
|
295
|
+
|
|
296
|
+
const collectRefs = (node: any, visited: Set<string>): void => {
|
|
297
|
+
if (!node || typeof node !== "object") return;
|
|
298
|
+
|
|
299
|
+
if (node.$ref && typeof node.$ref === "string" && !node.$ref.startsWith("#")) {
|
|
300
|
+
const refId = node.$ref;
|
|
301
|
+
if (!visited.has(refId) && this.allSchemas![refId]) {
|
|
302
|
+
visited.add(refId);
|
|
303
|
+
// Clone and strip top-level JSON Schema meta fields not valid inside $defs
|
|
304
|
+
const def = JSON.parse(JSON.stringify(this.allSchemas![refId]));
|
|
305
|
+
delete def.$id;
|
|
306
|
+
delete def.$schema;
|
|
307
|
+
defs[refId] = def;
|
|
308
|
+
// Recurse into the referenced schema to collect its deps
|
|
309
|
+
collectRefs(def, visited);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
for (const value of Object.values(node)) {
|
|
314
|
+
collectRefs(value, visited);
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const visited = new Set<string>();
|
|
319
|
+
collectRefs(schema, visited);
|
|
320
|
+
|
|
321
|
+
if (Object.keys(defs).length === 0) return schema;
|
|
322
|
+
|
|
323
|
+
// Deep clone and rewrite all non-standard $ref values to #/$defs/<id>
|
|
324
|
+
const resolved = JSON.parse(JSON.stringify(schema));
|
|
325
|
+
delete resolved.$id;
|
|
326
|
+
delete resolved.$schema;
|
|
327
|
+
|
|
328
|
+
const rewriteRefs = (node: any): void => {
|
|
329
|
+
if (!node || typeof node !== "object") return;
|
|
330
|
+
|
|
331
|
+
if (node.$ref && typeof node.$ref === "string" && !node.$ref.startsWith("#")) {
|
|
332
|
+
node.$ref = `#/$defs/${node.$ref}`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
for (const value of Object.values(node)) {
|
|
336
|
+
rewriteRefs(value);
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
rewriteRefs(resolved);
|
|
341
|
+
// Also rewrite refs inside the collected defs
|
|
342
|
+
for (const def of Object.values(defs)) {
|
|
343
|
+
rewriteRefs(def);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
resolved.$defs = defs;
|
|
347
|
+
return resolved;
|
|
348
|
+
}
|
|
349
|
+
|
|
283
350
|
/**
|
|
284
351
|
* Get tool result for AI consumption
|
|
285
352
|
* Formats ToolResult into string for AI context
|
|
@@ -41,6 +41,7 @@ export type InstructionsReturn = string | { file: string; params?: Record<string
|
|
|
41
41
|
|
|
42
42
|
interface FileCacheEntry {
|
|
43
43
|
content: string;
|
|
44
|
+
resolvedContent: string;
|
|
44
45
|
mtime: number;
|
|
45
46
|
compiledTemplate?: Handlebars.TemplateDelegate;
|
|
46
47
|
hasTemplateExpressions?: boolean;
|
|
@@ -48,6 +49,28 @@ interface FileCacheEntry {
|
|
|
48
49
|
|
|
49
50
|
const fileCache = new Map<string, FileCacheEntry>();
|
|
50
51
|
|
|
52
|
+
const IMPORT_REGEX = /\{\{\s*import\s+["']([^"']+)['"]\s*\}\}/g;
|
|
53
|
+
|
|
54
|
+
function resolveImports(content: string, currentDir: string, visited: Set<string> = new Set()): string {
|
|
55
|
+
return content.replace(IMPORT_REGEX, (match, importPath) => {
|
|
56
|
+
const resolved = path.resolve(currentDir, importPath);
|
|
57
|
+
if (visited.has(resolved)) {
|
|
58
|
+
throw new Error(`Circular import detected: ${resolved}`);
|
|
59
|
+
}
|
|
60
|
+
const nextVisited = new Set(visited);
|
|
61
|
+
nextVisited.add(resolved);
|
|
62
|
+
try {
|
|
63
|
+
const importedContent = fs.readFileSync(resolved, "utf-8");
|
|
64
|
+
return resolveImports(importedContent, path.dirname(resolved), nextVisited);
|
|
65
|
+
} catch (err: any) {
|
|
66
|
+
if (err.code === "ENOENT") {
|
|
67
|
+
throw new Error(`Imported markdown file not found: ${resolved} (imported from: ${currentDir})`);
|
|
68
|
+
}
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
51
74
|
/**
|
|
52
75
|
* Resolve a file path relative to project root (process.cwd()).
|
|
53
76
|
* Leading `./` and `/` are normalised away — all paths are treated as project-root-relative.
|
|
@@ -70,7 +93,8 @@ function loadFile(filePath: string): FileCacheEntry {
|
|
|
70
93
|
}
|
|
71
94
|
|
|
72
95
|
const content = fs.readFileSync(resolvedPath, "utf-8");
|
|
73
|
-
const
|
|
96
|
+
const resolvedContent = resolveImports(content, path.dirname(resolvedPath));
|
|
97
|
+
const entry: FileCacheEntry = { content, resolvedContent, mtime };
|
|
74
98
|
fileCache.set(resolvedPath, entry);
|
|
75
99
|
return entry;
|
|
76
100
|
} catch (err: any) {
|
|
@@ -83,15 +107,15 @@ function loadFile(filePath: string): FileCacheEntry {
|
|
|
83
107
|
|
|
84
108
|
function renderTemplate(entry: FileCacheEntry, data: any): string {
|
|
85
109
|
if (entry.hasTemplateExpressions === false) {
|
|
86
|
-
return entry.
|
|
110
|
+
return entry.resolvedContent;
|
|
87
111
|
}
|
|
88
112
|
|
|
89
113
|
if (!entry.compiledTemplate) {
|
|
90
|
-
entry.hasTemplateExpressions = /\{\{/.test(entry.
|
|
114
|
+
entry.hasTemplateExpressions = /\{\{/.test(entry.resolvedContent);
|
|
91
115
|
if (!entry.hasTemplateExpressions) {
|
|
92
|
-
return entry.
|
|
116
|
+
return entry.resolvedContent;
|
|
93
117
|
}
|
|
94
|
-
entry.compiledTemplate = Handlebars.compile(entry.
|
|
118
|
+
entry.compiledTemplate = Handlebars.compile(entry.resolvedContent);
|
|
95
119
|
}
|
|
96
120
|
|
|
97
121
|
return entry.compiledTemplate(data);
|