@teamkeel/functions-runtime 0.458.0 → 0.460.0
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/dist/index.cjs +1321 -302
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +316 -9
- package/dist/index.d.ts +316 -9
- package/dist/index.js +1334 -305
- package/dist/index.js.map +1 -1
- package/package.json +11 -2
package/dist/index.cjs
CHANGED
|
@@ -6,6 +6,9 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
8
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
9
|
+
var __esm = (fn, res) => function __init() {
|
|
10
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
|
+
};
|
|
9
12
|
var __export = (target, all) => {
|
|
10
13
|
for (var name in all)
|
|
11
14
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -28,6 +31,323 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
28
31
|
));
|
|
29
32
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
33
|
|
|
34
|
+
// src/File.ts
|
|
35
|
+
var File_exports = {};
|
|
36
|
+
__export(File_exports, {
|
|
37
|
+
File: () => File,
|
|
38
|
+
InlineFile: () => InlineFile,
|
|
39
|
+
buildContentDisposition: () => buildContentDisposition,
|
|
40
|
+
deleteStoredFile: () => deleteStoredFile,
|
|
41
|
+
rewriteFilesDomain: () => rewriteFilesDomain
|
|
42
|
+
});
|
|
43
|
+
function rewriteFilesDomain(url) {
|
|
44
|
+
const domain = process.env.KEEL_FILES_DOMAIN;
|
|
45
|
+
if (domain) {
|
|
46
|
+
const override = new URL(domain);
|
|
47
|
+
url.protocol = override.protocol;
|
|
48
|
+
url.hostname = override.hostname;
|
|
49
|
+
url.port = override.port;
|
|
50
|
+
const overridePath = override.pathname.replace(/\/+$/, "");
|
|
51
|
+
if (overridePath && overridePath !== "/") {
|
|
52
|
+
url.pathname = overridePath + url.pathname;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return url;
|
|
56
|
+
}
|
|
57
|
+
function encodeRFC5987(value) {
|
|
58
|
+
return encodeURIComponent(value).replace(
|
|
59
|
+
/['()*]/g,
|
|
60
|
+
(c) => "%" + c.charCodeAt(0).toString(16).toUpperCase()
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
function buildContentDisposition(disposition, filename) {
|
|
64
|
+
const type = disposition === "attachment" ? "attachment" : "inline";
|
|
65
|
+
if (!filename || filename.trim() === "") {
|
|
66
|
+
return type;
|
|
67
|
+
}
|
|
68
|
+
const asciiFallback = filename.replace(/[^\x20-\x7e]|["\\]/g, "_");
|
|
69
|
+
return `${type}; filename="${asciiFallback}"; filename*=UTF-8''${encodeRFC5987(
|
|
70
|
+
filename
|
|
71
|
+
)}`;
|
|
72
|
+
}
|
|
73
|
+
async function storeFile(contents, key, filename, contentType, size, expires) {
|
|
74
|
+
if (!s3Client) {
|
|
75
|
+
throw new Error("S3 client is required");
|
|
76
|
+
}
|
|
77
|
+
const params = {
|
|
78
|
+
Bucket: process.env.KEEL_FILES_BUCKET_NAME,
|
|
79
|
+
Key: "files/" + key,
|
|
80
|
+
Body: contents,
|
|
81
|
+
ContentType: contentType,
|
|
82
|
+
ContentDisposition: `attachment; filename="${encodeURIComponent(
|
|
83
|
+
filename
|
|
84
|
+
)}"`,
|
|
85
|
+
Metadata: {
|
|
86
|
+
filename
|
|
87
|
+
},
|
|
88
|
+
ACL: "private"
|
|
89
|
+
};
|
|
90
|
+
if (expires) {
|
|
91
|
+
if (expires instanceof Date) {
|
|
92
|
+
params.Expires = expires;
|
|
93
|
+
} else {
|
|
94
|
+
console.warn("Invalid expires value. Skipping Expires parameter.");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const command = new import_client_s3.PutObjectCommand(params);
|
|
98
|
+
try {
|
|
99
|
+
await s3Client.send(command);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error("Error uploading file:", error);
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function deleteStoredFile(key) {
|
|
106
|
+
if (!s3Client) {
|
|
107
|
+
throw new Error("S3 client is required");
|
|
108
|
+
}
|
|
109
|
+
await s3Client.send(
|
|
110
|
+
new import_client_s3.DeleteObjectCommand({
|
|
111
|
+
Bucket: process.env.KEEL_FILES_BUCKET_NAME,
|
|
112
|
+
Key: "files/" + key
|
|
113
|
+
})
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
var import_client_s3, import_credential_providers, import_s3_request_presigner, import_ksuid, s3Client, InlineFile, File;
|
|
117
|
+
var init_File = __esm({
|
|
118
|
+
"src/File.ts"() {
|
|
119
|
+
"use strict";
|
|
120
|
+
import_client_s3 = require("@aws-sdk/client-s3");
|
|
121
|
+
import_credential_providers = require("@aws-sdk/credential-providers");
|
|
122
|
+
import_s3_request_presigner = require("@aws-sdk/s3-request-presigner");
|
|
123
|
+
import_ksuid = __toESM(require("ksuid"), 1);
|
|
124
|
+
s3Client = (() => {
|
|
125
|
+
if (!process.env.KEEL_FILES_BUCKET_NAME) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
const endpoint = process.env.KEEL_S3_ENDPOINT;
|
|
129
|
+
if (endpoint) {
|
|
130
|
+
return new import_client_s3.S3Client({
|
|
131
|
+
region: process.env.KEEL_REGION,
|
|
132
|
+
credentials: {
|
|
133
|
+
accessKeyId: "keelstorage",
|
|
134
|
+
secretAccessKey: "keelstorage"
|
|
135
|
+
},
|
|
136
|
+
endpoint
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
const testEndpoint = process.env.TEST_AWS_ENDPOINT;
|
|
140
|
+
if (testEndpoint) {
|
|
141
|
+
return new import_client_s3.S3Client({
|
|
142
|
+
region: process.env.KEEL_REGION,
|
|
143
|
+
credentials: {
|
|
144
|
+
accessKeyId: "test",
|
|
145
|
+
secretAccessKey: "test"
|
|
146
|
+
},
|
|
147
|
+
endpointProvider: /* @__PURE__ */ __name(() => {
|
|
148
|
+
return {
|
|
149
|
+
url: new URL(testEndpoint)
|
|
150
|
+
};
|
|
151
|
+
}, "endpointProvider")
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return new import_client_s3.S3Client({
|
|
155
|
+
region: process.env.KEEL_REGION,
|
|
156
|
+
credentials: (0, import_credential_providers.fromEnv)()
|
|
157
|
+
});
|
|
158
|
+
})();
|
|
159
|
+
__name(rewriteFilesDomain, "rewriteFilesDomain");
|
|
160
|
+
__name(encodeRFC5987, "encodeRFC5987");
|
|
161
|
+
__name(buildContentDisposition, "buildContentDisposition");
|
|
162
|
+
InlineFile = class _InlineFile {
|
|
163
|
+
static {
|
|
164
|
+
__name(this, "InlineFile");
|
|
165
|
+
}
|
|
166
|
+
constructor(input) {
|
|
167
|
+
this._filename = input.filename;
|
|
168
|
+
this._contentType = input.contentType;
|
|
169
|
+
this._contents = null;
|
|
170
|
+
}
|
|
171
|
+
static fromDataURL(dataURL) {
|
|
172
|
+
const info = dataURL.split(",")[0].split(":")[1];
|
|
173
|
+
const data = dataURL.split(",")[1];
|
|
174
|
+
const mimeType = info.split(";")[0];
|
|
175
|
+
const name = info.split(";")[1].split("=")[1] || "file";
|
|
176
|
+
const buffer = Buffer.from(data, "base64");
|
|
177
|
+
const file2 = new _InlineFile({ filename: name, contentType: mimeType });
|
|
178
|
+
file2.write(buffer);
|
|
179
|
+
return file2;
|
|
180
|
+
}
|
|
181
|
+
// Gets size of the file's contents in bytes
|
|
182
|
+
get size() {
|
|
183
|
+
if (this._contents) {
|
|
184
|
+
return this._contents.size;
|
|
185
|
+
}
|
|
186
|
+
return 0;
|
|
187
|
+
}
|
|
188
|
+
// Gets the media type of the file contents
|
|
189
|
+
get contentType() {
|
|
190
|
+
return this._contentType;
|
|
191
|
+
}
|
|
192
|
+
// Gets the name of the file
|
|
193
|
+
get filename() {
|
|
194
|
+
return this._filename;
|
|
195
|
+
}
|
|
196
|
+
// Write the files contents from a buffer
|
|
197
|
+
write(buffer) {
|
|
198
|
+
this._contents = new Blob([
|
|
199
|
+
new Uint8Array(
|
|
200
|
+
buffer.buffer,
|
|
201
|
+
buffer.byteOffset,
|
|
202
|
+
buffer.byteLength
|
|
203
|
+
)
|
|
204
|
+
]);
|
|
205
|
+
}
|
|
206
|
+
// Reads the contents of the file as a buffer
|
|
207
|
+
async read() {
|
|
208
|
+
if (!this._contents) {
|
|
209
|
+
throw new Error("No contents to read");
|
|
210
|
+
}
|
|
211
|
+
const arrayBuffer = await this._contents.arrayBuffer();
|
|
212
|
+
return Buffer.from(arrayBuffer);
|
|
213
|
+
}
|
|
214
|
+
// Persists the file
|
|
215
|
+
async store(expires = null) {
|
|
216
|
+
const content = await this.read();
|
|
217
|
+
const key = import_ksuid.default.randomSync().string;
|
|
218
|
+
await storeFile(
|
|
219
|
+
content,
|
|
220
|
+
key,
|
|
221
|
+
this._filename,
|
|
222
|
+
this._contentType,
|
|
223
|
+
this.size,
|
|
224
|
+
expires
|
|
225
|
+
);
|
|
226
|
+
return new File({
|
|
227
|
+
key,
|
|
228
|
+
size: this.size,
|
|
229
|
+
filename: this.filename,
|
|
230
|
+
contentType: this.contentType
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
File = class _File extends InlineFile {
|
|
235
|
+
static {
|
|
236
|
+
__name(this, "File");
|
|
237
|
+
}
|
|
238
|
+
constructor(input) {
|
|
239
|
+
super({
|
|
240
|
+
filename: input.filename || "",
|
|
241
|
+
contentType: input.contentType || ""
|
|
242
|
+
});
|
|
243
|
+
this._key = input.key || "";
|
|
244
|
+
this._size = input.size || 0;
|
|
245
|
+
}
|
|
246
|
+
// Creates a new instance from the database record
|
|
247
|
+
static fromDbRecord(input) {
|
|
248
|
+
return new _File({
|
|
249
|
+
key: input.key,
|
|
250
|
+
filename: input.filename,
|
|
251
|
+
size: input.size,
|
|
252
|
+
contentType: input.contentType
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
get size() {
|
|
256
|
+
return this._size;
|
|
257
|
+
}
|
|
258
|
+
// Gets the stored key
|
|
259
|
+
get key() {
|
|
260
|
+
return this._key;
|
|
261
|
+
}
|
|
262
|
+
get isPublic() {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
async read() {
|
|
266
|
+
if (this._contents) {
|
|
267
|
+
const arrayBuffer = await this._contents.arrayBuffer();
|
|
268
|
+
return Buffer.from(arrayBuffer);
|
|
269
|
+
}
|
|
270
|
+
if (!s3Client) {
|
|
271
|
+
throw new Error("S3 client is required");
|
|
272
|
+
}
|
|
273
|
+
const params = {
|
|
274
|
+
Bucket: process.env.KEEL_FILES_BUCKET_NAME,
|
|
275
|
+
Key: "files/" + this.key
|
|
276
|
+
};
|
|
277
|
+
const command = new import_client_s3.GetObjectCommand(params);
|
|
278
|
+
const response = await s3Client.send(command);
|
|
279
|
+
const blob = await response.Body.transformToByteArray();
|
|
280
|
+
return Buffer.from(blob);
|
|
281
|
+
}
|
|
282
|
+
async store(expires = null) {
|
|
283
|
+
if (this._contents) {
|
|
284
|
+
const contents = await this.read();
|
|
285
|
+
await storeFile(
|
|
286
|
+
contents,
|
|
287
|
+
this.key,
|
|
288
|
+
this.filename,
|
|
289
|
+
this.contentType,
|
|
290
|
+
this.size,
|
|
291
|
+
expires
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
return this;
|
|
295
|
+
}
|
|
296
|
+
// Generates a presigned download URL.
|
|
297
|
+
//
|
|
298
|
+
// By default the browser previews the file inline and, when saved, uses the
|
|
299
|
+
// file's own filename. Pass `contentDisposition: "attachment"` to force a
|
|
300
|
+
// download, or `filename` to override the suggested name.
|
|
301
|
+
async getPresignedUrl(options) {
|
|
302
|
+
if (!s3Client) {
|
|
303
|
+
throw new Error("S3 client is required");
|
|
304
|
+
}
|
|
305
|
+
const disposition = options?.contentDisposition ?? "inline";
|
|
306
|
+
const filename = options?.filename ?? this.filename;
|
|
307
|
+
const command = new import_client_s3.GetObjectCommand({
|
|
308
|
+
Bucket: process.env.KEEL_FILES_BUCKET_NAME,
|
|
309
|
+
Key: "files/" + this.key,
|
|
310
|
+
ResponseContentDisposition: buildContentDisposition(
|
|
311
|
+
disposition,
|
|
312
|
+
filename
|
|
313
|
+
)
|
|
314
|
+
});
|
|
315
|
+
const url = await (0, import_s3_request_presigner.getSignedUrl)(s3Client, command, { expiresIn: 60 * 60 });
|
|
316
|
+
return rewriteFilesDomain(new URL(url));
|
|
317
|
+
}
|
|
318
|
+
// Generates a presigned upload URL. If the file doesn't have a key, a new one will be generated
|
|
319
|
+
async getPresignedUploadUrl() {
|
|
320
|
+
if (!s3Client) {
|
|
321
|
+
throw new Error("S3 client is required");
|
|
322
|
+
}
|
|
323
|
+
if (!this.key) {
|
|
324
|
+
this._key = import_ksuid.default.randomSync().string;
|
|
325
|
+
}
|
|
326
|
+
const command = new import_client_s3.PutObjectCommand({
|
|
327
|
+
Bucket: process.env.KEEL_FILES_BUCKET_NAME,
|
|
328
|
+
Key: "files/" + this.key
|
|
329
|
+
});
|
|
330
|
+
const url = await (0, import_s3_request_presigner.getSignedUrl)(s3Client, command, { expiresIn: 60 * 60 });
|
|
331
|
+
return rewriteFilesDomain(new URL(url));
|
|
332
|
+
}
|
|
333
|
+
// Persists the file
|
|
334
|
+
toDbRecord() {
|
|
335
|
+
return {
|
|
336
|
+
key: this.key,
|
|
337
|
+
filename: this.filename,
|
|
338
|
+
contentType: this.contentType,
|
|
339
|
+
size: this.size
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
toJSON() {
|
|
343
|
+
return this.toDbRecord();
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
__name(storeFile, "storeFile");
|
|
347
|
+
__name(deleteStoredFile, "deleteStoredFile");
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
31
351
|
// src/index.ts
|
|
32
352
|
var index_exports = {};
|
|
33
353
|
__export(index_exports, {
|
|
@@ -36,7 +356,7 @@ __export(index_exports, {
|
|
|
36
356
|
File: () => File,
|
|
37
357
|
FlowsAPI: () => FlowsAPI,
|
|
38
358
|
InlineFile: () => InlineFile,
|
|
39
|
-
KSUID: () =>
|
|
359
|
+
KSUID: () => import_ksuid4.default,
|
|
40
360
|
ModelAPI: () => ModelAPI,
|
|
41
361
|
NonRetriableError: () => NonRetriableError,
|
|
42
362
|
PERMISSION_STATE: () => PERMISSION_STATE,
|
|
@@ -51,15 +371,21 @@ __export(index_exports, {
|
|
|
51
371
|
TaskAPI: () => TaskAPI,
|
|
52
372
|
checkBuiltInPermissions: () => checkBuiltInPermissions,
|
|
53
373
|
createFlowContext: () => createFlowContext,
|
|
374
|
+
createIntegrationServer: () => createIntegrationServer,
|
|
375
|
+
createNotifier: () => createNotifier,
|
|
54
376
|
createTraceAPI: () => createTraceAPI2,
|
|
377
|
+
experimental: () => experimental_exports,
|
|
55
378
|
handleFlow: () => handleFlow,
|
|
56
379
|
handleJob: () => handleJob,
|
|
57
380
|
handleRequest: () => handleRequest,
|
|
58
381
|
handleRoute: () => handleRoute,
|
|
59
382
|
handleSubscriber: () => handleSubscriber,
|
|
383
|
+
insertNewStep: () => insertNewStep,
|
|
60
384
|
ksuid: () => ksuid,
|
|
385
|
+
notify: () => notify,
|
|
61
386
|
tracing: () => tracing_exports,
|
|
62
|
-
useDatabase: () => useDatabase
|
|
387
|
+
useDatabase: () => useDatabase,
|
|
388
|
+
z: () => import_zod.z
|
|
63
389
|
});
|
|
64
390
|
module.exports = __toCommonJS(index_exports);
|
|
65
391
|
|
|
@@ -130,6 +456,9 @@ var AuditContextPlugin = class {
|
|
|
130
456
|
const rawNode = import_kysely.sql`set_trace_id(${audit.traceId})`.as(this.traceIdAlias).toOperationNode();
|
|
131
457
|
returning.selections.push(import_kysely.SelectionNode.create(rawNode));
|
|
132
458
|
}
|
|
459
|
+
if (returning.selections.length === 0) {
|
|
460
|
+
return { ...args.node };
|
|
461
|
+
}
|
|
133
462
|
return {
|
|
134
463
|
...args.node,
|
|
135
464
|
returning
|
|
@@ -499,20 +828,44 @@ var KEEL_INTERNAL_CHILDREN = "includeChildrenSpans";
|
|
|
499
828
|
var import_ws = __toESM(require("ws"), 1);
|
|
500
829
|
var import_node_fs = require("fs");
|
|
501
830
|
var dbInstance = new import_node_async_hooks2.AsyncLocalStorage();
|
|
831
|
+
var fileCleanupStore = new import_node_async_hooks2.AsyncLocalStorage();
|
|
832
|
+
function deferFileDeletion(key) {
|
|
833
|
+
fileCleanupStore.getStore()?.add(key);
|
|
834
|
+
}
|
|
835
|
+
__name(deferFileDeletion, "deferFileDeletion");
|
|
836
|
+
async function flushFileDeletions(keys) {
|
|
837
|
+
if (keys.size === 0) {
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
const { deleteStoredFile: deleteStoredFile2 } = await Promise.resolve().then(() => (init_File(), File_exports));
|
|
841
|
+
for (const key of keys) {
|
|
842
|
+
try {
|
|
843
|
+
await deleteStoredFile2(key);
|
|
844
|
+
} catch (e) {
|
|
845
|
+
console.error("failed to delete orphaned file from storage", e);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
__name(flushFileDeletions, "flushFileDeletions");
|
|
502
850
|
var vitestDb = null;
|
|
503
851
|
async function withDatabase(db, requiresTransaction, cb) {
|
|
852
|
+
const pending = /* @__PURE__ */ new Set();
|
|
504
853
|
if (requiresTransaction) {
|
|
505
|
-
|
|
506
|
-
return dbInstance.run(transaction,
|
|
507
|
-
return cb({ transaction });
|
|
854
|
+
const result2 = await db.transaction().execute(async (transaction) => {
|
|
855
|
+
return dbInstance.run(transaction, () => {
|
|
856
|
+
return fileCleanupStore.run(pending, () => cb({ transaction }));
|
|
508
857
|
});
|
|
509
858
|
});
|
|
859
|
+
await flushFileDeletions(pending);
|
|
860
|
+
return result2;
|
|
510
861
|
}
|
|
511
|
-
|
|
512
|
-
return dbInstance.run(sDb,
|
|
513
|
-
return cb({ sDb });
|
|
862
|
+
const result = await db.connection().execute(async (sDb) => {
|
|
863
|
+
return dbInstance.run(sDb, () => {
|
|
864
|
+
return fileCleanupStore.run(pending, () => cb({ sDb }));
|
|
514
865
|
});
|
|
515
866
|
});
|
|
867
|
+
await flushFileDeletions(pending);
|
|
868
|
+
return result;
|
|
516
869
|
}
|
|
517
870
|
__name(withDatabase, "withDatabase");
|
|
518
871
|
function useDatabase() {
|
|
@@ -591,9 +944,9 @@ var InstrumentedClient = class extends import_pg.Client {
|
|
|
591
944
|
}
|
|
592
945
|
async query(...args) {
|
|
593
946
|
const _super = super.query.bind(this);
|
|
594
|
-
const
|
|
947
|
+
const sql5 = args[0];
|
|
595
948
|
let sqlAttribute = false;
|
|
596
|
-
let spanName = txStatements[
|
|
949
|
+
let spanName = txStatements[sql5.toLowerCase()];
|
|
597
950
|
if (!spanName) {
|
|
598
951
|
spanName = "Database Query";
|
|
599
952
|
sqlAttribute = true;
|
|
@@ -661,9 +1014,9 @@ function getDialect(connString) {
|
|
|
661
1014
|
pool.on("connect", (client) => {
|
|
662
1015
|
const originalQuery = client.query;
|
|
663
1016
|
client.query = function(...args) {
|
|
664
|
-
const
|
|
1017
|
+
const sql5 = args[0];
|
|
665
1018
|
let sqlAttribute = false;
|
|
666
|
-
let spanName = txStatements[
|
|
1019
|
+
let spanName = txStatements[sql5.toLowerCase()];
|
|
667
1020
|
if (!spanName) {
|
|
668
1021
|
spanName = "Database Query";
|
|
669
1022
|
sqlAttribute = true;
|
|
@@ -687,271 +1040,8 @@ function getDialect(connString) {
|
|
|
687
1040
|
}
|
|
688
1041
|
__name(getDialect, "getDialect");
|
|
689
1042
|
|
|
690
|
-
// src/File.ts
|
|
691
|
-
var import_client_s3 = require("@aws-sdk/client-s3");
|
|
692
|
-
var import_credential_providers = require("@aws-sdk/credential-providers");
|
|
693
|
-
var import_s3_request_presigner = require("@aws-sdk/s3-request-presigner");
|
|
694
|
-
var import_ksuid = __toESM(require("ksuid"), 1);
|
|
695
|
-
var s3Client = (() => {
|
|
696
|
-
if (!process.env.KEEL_FILES_BUCKET_NAME) {
|
|
697
|
-
return null;
|
|
698
|
-
}
|
|
699
|
-
const endpoint = process.env.KEEL_S3_ENDPOINT;
|
|
700
|
-
if (endpoint) {
|
|
701
|
-
return new import_client_s3.S3Client({
|
|
702
|
-
region: process.env.KEEL_REGION,
|
|
703
|
-
credentials: {
|
|
704
|
-
accessKeyId: "keelstorage",
|
|
705
|
-
secretAccessKey: "keelstorage"
|
|
706
|
-
},
|
|
707
|
-
endpoint
|
|
708
|
-
});
|
|
709
|
-
}
|
|
710
|
-
const testEndpoint = process.env.TEST_AWS_ENDPOINT;
|
|
711
|
-
if (testEndpoint) {
|
|
712
|
-
return new import_client_s3.S3Client({
|
|
713
|
-
region: process.env.KEEL_REGION,
|
|
714
|
-
credentials: {
|
|
715
|
-
accessKeyId: "test",
|
|
716
|
-
secretAccessKey: "test"
|
|
717
|
-
},
|
|
718
|
-
endpointProvider: /* @__PURE__ */ __name(() => {
|
|
719
|
-
return {
|
|
720
|
-
url: new URL(testEndpoint)
|
|
721
|
-
};
|
|
722
|
-
}, "endpointProvider")
|
|
723
|
-
});
|
|
724
|
-
}
|
|
725
|
-
return new import_client_s3.S3Client({
|
|
726
|
-
region: process.env.KEEL_REGION,
|
|
727
|
-
credentials: (0, import_credential_providers.fromEnv)()
|
|
728
|
-
});
|
|
729
|
-
})();
|
|
730
|
-
function rewriteFilesDomain(url) {
|
|
731
|
-
const domain = process.env.KEEL_FILES_DOMAIN;
|
|
732
|
-
if (domain) {
|
|
733
|
-
const override = new URL(domain);
|
|
734
|
-
url.protocol = override.protocol;
|
|
735
|
-
url.hostname = override.hostname;
|
|
736
|
-
url.port = override.port;
|
|
737
|
-
const overridePath = override.pathname.replace(/\/+$/, "");
|
|
738
|
-
if (overridePath && overridePath !== "/") {
|
|
739
|
-
url.pathname = overridePath + url.pathname;
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
return url;
|
|
743
|
-
}
|
|
744
|
-
__name(rewriteFilesDomain, "rewriteFilesDomain");
|
|
745
|
-
var InlineFile = class _InlineFile {
|
|
746
|
-
static {
|
|
747
|
-
__name(this, "InlineFile");
|
|
748
|
-
}
|
|
749
|
-
constructor(input) {
|
|
750
|
-
this._filename = input.filename;
|
|
751
|
-
this._contentType = input.contentType;
|
|
752
|
-
this._contents = null;
|
|
753
|
-
}
|
|
754
|
-
static fromDataURL(dataURL) {
|
|
755
|
-
const info = dataURL.split(",")[0].split(":")[1];
|
|
756
|
-
const data = dataURL.split(",")[1];
|
|
757
|
-
const mimeType = info.split(";")[0];
|
|
758
|
-
const name = info.split(";")[1].split("=")[1] || "file";
|
|
759
|
-
const buffer = Buffer.from(data, "base64");
|
|
760
|
-
const file2 = new _InlineFile({ filename: name, contentType: mimeType });
|
|
761
|
-
file2.write(buffer);
|
|
762
|
-
return file2;
|
|
763
|
-
}
|
|
764
|
-
// Gets size of the file's contents in bytes
|
|
765
|
-
get size() {
|
|
766
|
-
if (this._contents) {
|
|
767
|
-
return this._contents.size;
|
|
768
|
-
}
|
|
769
|
-
return 0;
|
|
770
|
-
}
|
|
771
|
-
// Gets the media type of the file contents
|
|
772
|
-
get contentType() {
|
|
773
|
-
return this._contentType;
|
|
774
|
-
}
|
|
775
|
-
// Gets the name of the file
|
|
776
|
-
get filename() {
|
|
777
|
-
return this._filename;
|
|
778
|
-
}
|
|
779
|
-
// Write the files contents from a buffer
|
|
780
|
-
write(buffer) {
|
|
781
|
-
this._contents = new Blob([
|
|
782
|
-
new Uint8Array(
|
|
783
|
-
buffer.buffer,
|
|
784
|
-
buffer.byteOffset,
|
|
785
|
-
buffer.byteLength
|
|
786
|
-
)
|
|
787
|
-
]);
|
|
788
|
-
}
|
|
789
|
-
// Reads the contents of the file as a buffer
|
|
790
|
-
async read() {
|
|
791
|
-
if (!this._contents) {
|
|
792
|
-
throw new Error("No contents to read");
|
|
793
|
-
}
|
|
794
|
-
const arrayBuffer = await this._contents.arrayBuffer();
|
|
795
|
-
return Buffer.from(arrayBuffer);
|
|
796
|
-
}
|
|
797
|
-
// Persists the file
|
|
798
|
-
async store(expires = null) {
|
|
799
|
-
const content = await this.read();
|
|
800
|
-
const key = import_ksuid.default.randomSync().string;
|
|
801
|
-
await storeFile(
|
|
802
|
-
content,
|
|
803
|
-
key,
|
|
804
|
-
this._filename,
|
|
805
|
-
this._contentType,
|
|
806
|
-
this.size,
|
|
807
|
-
expires
|
|
808
|
-
);
|
|
809
|
-
return new File({
|
|
810
|
-
key,
|
|
811
|
-
size: this.size,
|
|
812
|
-
filename: this.filename,
|
|
813
|
-
contentType: this.contentType
|
|
814
|
-
});
|
|
815
|
-
}
|
|
816
|
-
};
|
|
817
|
-
var File = class _File extends InlineFile {
|
|
818
|
-
static {
|
|
819
|
-
__name(this, "File");
|
|
820
|
-
}
|
|
821
|
-
constructor(input) {
|
|
822
|
-
super({
|
|
823
|
-
filename: input.filename || "",
|
|
824
|
-
contentType: input.contentType || ""
|
|
825
|
-
});
|
|
826
|
-
this._key = input.key || "";
|
|
827
|
-
this._size = input.size || 0;
|
|
828
|
-
}
|
|
829
|
-
// Creates a new instance from the database record
|
|
830
|
-
static fromDbRecord(input) {
|
|
831
|
-
return new _File({
|
|
832
|
-
key: input.key,
|
|
833
|
-
filename: input.filename,
|
|
834
|
-
size: input.size,
|
|
835
|
-
contentType: input.contentType
|
|
836
|
-
});
|
|
837
|
-
}
|
|
838
|
-
get size() {
|
|
839
|
-
return this._size;
|
|
840
|
-
}
|
|
841
|
-
// Gets the stored key
|
|
842
|
-
get key() {
|
|
843
|
-
return this._key;
|
|
844
|
-
}
|
|
845
|
-
get isPublic() {
|
|
846
|
-
return false;
|
|
847
|
-
}
|
|
848
|
-
async read() {
|
|
849
|
-
if (this._contents) {
|
|
850
|
-
const arrayBuffer = await this._contents.arrayBuffer();
|
|
851
|
-
return Buffer.from(arrayBuffer);
|
|
852
|
-
}
|
|
853
|
-
if (!s3Client) {
|
|
854
|
-
throw new Error("S3 client is required");
|
|
855
|
-
}
|
|
856
|
-
const params = {
|
|
857
|
-
Bucket: process.env.KEEL_FILES_BUCKET_NAME,
|
|
858
|
-
Key: "files/" + this.key
|
|
859
|
-
};
|
|
860
|
-
const command = new import_client_s3.GetObjectCommand(params);
|
|
861
|
-
const response = await s3Client.send(command);
|
|
862
|
-
const blob = await response.Body.transformToByteArray();
|
|
863
|
-
return Buffer.from(blob);
|
|
864
|
-
}
|
|
865
|
-
async store(expires = null) {
|
|
866
|
-
if (this._contents) {
|
|
867
|
-
const contents = await this.read();
|
|
868
|
-
await storeFile(
|
|
869
|
-
contents,
|
|
870
|
-
this.key,
|
|
871
|
-
this.filename,
|
|
872
|
-
this.contentType,
|
|
873
|
-
this.size,
|
|
874
|
-
expires
|
|
875
|
-
);
|
|
876
|
-
}
|
|
877
|
-
return this;
|
|
878
|
-
}
|
|
879
|
-
// Generates a presigned download URL
|
|
880
|
-
async getPresignedUrl() {
|
|
881
|
-
if (!s3Client) {
|
|
882
|
-
throw new Error("S3 client is required");
|
|
883
|
-
}
|
|
884
|
-
const command = new import_client_s3.GetObjectCommand({
|
|
885
|
-
Bucket: process.env.KEEL_FILES_BUCKET_NAME,
|
|
886
|
-
Key: "files/" + this.key,
|
|
887
|
-
ResponseContentDisposition: "inline"
|
|
888
|
-
});
|
|
889
|
-
const url = await (0, import_s3_request_presigner.getSignedUrl)(s3Client, command, { expiresIn: 60 * 60 });
|
|
890
|
-
return rewriteFilesDomain(new URL(url));
|
|
891
|
-
}
|
|
892
|
-
// Generates a presigned upload URL. If the file doesn't have a key, a new one will be generated
|
|
893
|
-
async getPresignedUploadUrl() {
|
|
894
|
-
if (!s3Client) {
|
|
895
|
-
throw new Error("S3 client is required");
|
|
896
|
-
}
|
|
897
|
-
if (!this.key) {
|
|
898
|
-
this._key = import_ksuid.default.randomSync().string;
|
|
899
|
-
}
|
|
900
|
-
const command = new import_client_s3.PutObjectCommand({
|
|
901
|
-
Bucket: process.env.KEEL_FILES_BUCKET_NAME,
|
|
902
|
-
Key: "files/" + this.key
|
|
903
|
-
});
|
|
904
|
-
const url = await (0, import_s3_request_presigner.getSignedUrl)(s3Client, command, { expiresIn: 60 * 60 });
|
|
905
|
-
return rewriteFilesDomain(new URL(url));
|
|
906
|
-
}
|
|
907
|
-
// Persists the file
|
|
908
|
-
toDbRecord() {
|
|
909
|
-
return {
|
|
910
|
-
key: this.key,
|
|
911
|
-
filename: this.filename,
|
|
912
|
-
contentType: this.contentType,
|
|
913
|
-
size: this.size
|
|
914
|
-
};
|
|
915
|
-
}
|
|
916
|
-
toJSON() {
|
|
917
|
-
return this.toDbRecord();
|
|
918
|
-
}
|
|
919
|
-
};
|
|
920
|
-
async function storeFile(contents, key, filename, contentType, size, expires) {
|
|
921
|
-
if (!s3Client) {
|
|
922
|
-
throw new Error("S3 client is required");
|
|
923
|
-
}
|
|
924
|
-
const params = {
|
|
925
|
-
Bucket: process.env.KEEL_FILES_BUCKET_NAME,
|
|
926
|
-
Key: "files/" + key,
|
|
927
|
-
Body: contents,
|
|
928
|
-
ContentType: contentType,
|
|
929
|
-
ContentDisposition: `attachment; filename="${encodeURIComponent(
|
|
930
|
-
filename
|
|
931
|
-
)}"`,
|
|
932
|
-
Metadata: {
|
|
933
|
-
filename
|
|
934
|
-
},
|
|
935
|
-
ACL: "private"
|
|
936
|
-
};
|
|
937
|
-
if (expires) {
|
|
938
|
-
if (expires instanceof Date) {
|
|
939
|
-
params.Expires = expires;
|
|
940
|
-
} else {
|
|
941
|
-
console.warn("Invalid expires value. Skipping Expires parameter.");
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
const command = new import_client_s3.PutObjectCommand(params);
|
|
945
|
-
try {
|
|
946
|
-
await s3Client.send(command);
|
|
947
|
-
} catch (error) {
|
|
948
|
-
console.error("Error uploading file:", error);
|
|
949
|
-
throw error;
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
__name(storeFile, "storeFile");
|
|
953
|
-
|
|
954
1043
|
// src/parsing.js
|
|
1044
|
+
init_File();
|
|
955
1045
|
var dateFormat = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
|
|
956
1046
|
function parseInputs(inputs) {
|
|
957
1047
|
if (inputs != null && typeof inputs === "object") {
|
|
@@ -1154,23 +1244,23 @@ var TimePeriod = class _TimePeriod {
|
|
|
1154
1244
|
return new _TimePeriod(period, value, offset, complete2);
|
|
1155
1245
|
}
|
|
1156
1246
|
periodStartSQL() {
|
|
1157
|
-
let
|
|
1247
|
+
let sql5 = "NOW()";
|
|
1158
1248
|
if (this.offset !== 0) {
|
|
1159
|
-
|
|
1249
|
+
sql5 = `${sql5} + INTERVAL '${this.offset} ${this.period}'`;
|
|
1160
1250
|
}
|
|
1161
1251
|
if (this.complete) {
|
|
1162
|
-
|
|
1252
|
+
sql5 = `DATE_TRUNC('${this.period}', ${sql5})`;
|
|
1163
1253
|
} else {
|
|
1164
|
-
|
|
1254
|
+
sql5 = `(${sql5})`;
|
|
1165
1255
|
}
|
|
1166
|
-
return
|
|
1256
|
+
return sql5;
|
|
1167
1257
|
}
|
|
1168
1258
|
periodEndSQL() {
|
|
1169
|
-
let
|
|
1259
|
+
let sql5 = this.periodStartSQL();
|
|
1170
1260
|
if (this.value != 0) {
|
|
1171
|
-
|
|
1261
|
+
sql5 = `(${sql5} + INTERVAL '${this.value} ${this.period}')`;
|
|
1172
1262
|
}
|
|
1173
|
-
return
|
|
1263
|
+
return sql5;
|
|
1174
1264
|
}
|
|
1175
1265
|
};
|
|
1176
1266
|
|
|
@@ -1884,6 +1974,7 @@ var QueryBuilder = class _QueryBuilder {
|
|
|
1884
1974
|
};
|
|
1885
1975
|
|
|
1886
1976
|
// src/ModelAPI.js
|
|
1977
|
+
init_File();
|
|
1887
1978
|
var ModelAPI = class {
|
|
1888
1979
|
static {
|
|
1889
1980
|
__name(this, "ModelAPI");
|
|
@@ -1893,9 +1984,10 @@ var ModelAPI = class {
|
|
|
1893
1984
|
* @param {Function} _ Used to be a function that returns the default values for a row in this table. No longer used.
|
|
1894
1985
|
* @param {TableConfigMap} tableConfigMap
|
|
1895
1986
|
*/
|
|
1896
|
-
constructor(tableName, _, tableConfigMap = {}) {
|
|
1987
|
+
constructor(tableName, _, tableConfigMap = {}, fileFieldsMap = {}) {
|
|
1897
1988
|
this._tableName = tableName;
|
|
1898
1989
|
this._tableConfigMap = tableConfigMap;
|
|
1990
|
+
this._fileFields = fileFieldsMap[tableName] || {};
|
|
1899
1991
|
this._modelName = upperCamelCase(this._tableName);
|
|
1900
1992
|
}
|
|
1901
1993
|
async create(values) {
|
|
@@ -1965,6 +2057,10 @@ var ModelAPI = class {
|
|
|
1965
2057
|
const name = spanNameForModelAPI(this._modelName, "update");
|
|
1966
2058
|
const db = useDatabase();
|
|
1967
2059
|
return withSpan(name, async (span) => {
|
|
2060
|
+
const fileColumns = Object.keys(values || {}).filter(
|
|
2061
|
+
(k) => k in this._fileFields
|
|
2062
|
+
);
|
|
2063
|
+
const existingFileRows = fileColumns.length > 0 ? await this._selectExistingFileValues(where, fileColumns) : [];
|
|
1968
2064
|
let builder = db.updateTable(this._tableName).returningAll();
|
|
1969
2065
|
const keys = values ? Object.keys(values) : [];
|
|
1970
2066
|
const row = {};
|
|
@@ -2003,7 +2099,9 @@ var ModelAPI = class {
|
|
|
2003
2099
|
span.setAttribute("sql", builder.compile().sql);
|
|
2004
2100
|
try {
|
|
2005
2101
|
const row2 = await builder.executeTakeFirstOrThrow();
|
|
2006
|
-
|
|
2102
|
+
const result = transformRichDataTypes(camelCaseObject(row2));
|
|
2103
|
+
this._deferReplacedFiles(existingFileRows, fileColumns, result);
|
|
2104
|
+
return result;
|
|
2007
2105
|
} catch (e) {
|
|
2008
2106
|
throw new DatabaseError(e);
|
|
2009
2107
|
}
|
|
@@ -2013,12 +2111,15 @@ var ModelAPI = class {
|
|
|
2013
2111
|
const name = spanNameForModelAPI(this._modelName, "delete");
|
|
2014
2112
|
const db = useDatabase();
|
|
2015
2113
|
return withSpan(name, async (span) => {
|
|
2114
|
+
const fileColumns = Object.keys(this._fileFields);
|
|
2115
|
+
const existingFileRows = fileColumns.length > 0 ? await this._selectExistingFileValues(where, fileColumns) : [];
|
|
2016
2116
|
let builder = db.deleteFrom(this._tableName).returning(["id"]);
|
|
2017
2117
|
const context7 = new QueryContext([this._tableName], this._tableConfigMap);
|
|
2018
2118
|
builder = applyWhereConditions(context7, builder, where);
|
|
2019
2119
|
span.setAttribute("sql", builder.compile().sql);
|
|
2020
2120
|
try {
|
|
2021
2121
|
const row = await builder.executeTakeFirstOrThrow();
|
|
2122
|
+
this._deferReplacedFiles(existingFileRows, fileColumns, null);
|
|
2022
2123
|
return row.id;
|
|
2023
2124
|
} catch (e) {
|
|
2024
2125
|
throw new DatabaseError(e);
|
|
@@ -2033,6 +2134,30 @@ var ModelAPI = class {
|
|
|
2033
2134
|
builder = applyWhereConditions(context7, builder, where);
|
|
2034
2135
|
return new QueryBuilder(this._tableName, context7, builder);
|
|
2035
2136
|
}
|
|
2137
|
+
// Reads the current file-column values for rows matched by `where`.
|
|
2138
|
+
async _selectExistingFileValues(where, fileColumns) {
|
|
2139
|
+
if (fileColumns.length === 0) {
|
|
2140
|
+
return [];
|
|
2141
|
+
}
|
|
2142
|
+
const db = useDatabase();
|
|
2143
|
+
let builder = db.selectFrom(this._tableName).selectAll(this._tableName);
|
|
2144
|
+
const context7 = new QueryContext([this._tableName], this._tableConfigMap);
|
|
2145
|
+
builder = applyWhereConditions(context7, builder, where);
|
|
2146
|
+
const rows = await builder.execute();
|
|
2147
|
+
return rows.map((x) => transformRichDataTypes(camelCaseObject(x)));
|
|
2148
|
+
}
|
|
2149
|
+
// Defers deletion of every old file key in existingRows that is not still
|
|
2150
|
+
// referenced by newRow.
|
|
2151
|
+
_deferReplacedFiles(existingRows, fileColumns, newRow) {
|
|
2152
|
+
const retained = new Set(
|
|
2153
|
+
collectFileKeys(newRow ? [newRow] : [], fileColumns)
|
|
2154
|
+
);
|
|
2155
|
+
for (const key of collectFileKeys(existingRows, fileColumns)) {
|
|
2156
|
+
if (!retained.has(key)) {
|
|
2157
|
+
deferFileDeletion(key);
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2036
2161
|
};
|
|
2037
2162
|
async function create(conn, tableName, tableConfigs, values) {
|
|
2038
2163
|
try {
|
|
@@ -2142,6 +2267,23 @@ async function create(conn, tableName, tableConfigs, values) {
|
|
|
2142
2267
|
}
|
|
2143
2268
|
}
|
|
2144
2269
|
__name(create, "create");
|
|
2270
|
+
function collectFileKeys(rows, fileColumns) {
|
|
2271
|
+
const keys = [];
|
|
2272
|
+
for (const row of rows || []) {
|
|
2273
|
+
if (!row) continue;
|
|
2274
|
+
for (const col of fileColumns) {
|
|
2275
|
+
const v = row[col];
|
|
2276
|
+
if (!v) continue;
|
|
2277
|
+
if (Array.isArray(v)) {
|
|
2278
|
+
for (const f of v) if (f?.key) keys.push(f.key);
|
|
2279
|
+
} else if (v.key) {
|
|
2280
|
+
keys.push(v.key);
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
return keys;
|
|
2285
|
+
}
|
|
2286
|
+
__name(collectFileKeys, "collectFileKeys");
|
|
2145
2287
|
|
|
2146
2288
|
// src/TaskAPI.js
|
|
2147
2289
|
var import_jsonwebtoken = __toESM(require("jsonwebtoken"), 1);
|
|
@@ -2687,6 +2829,51 @@ var FlowsAPI = class _FlowsAPI {
|
|
|
2687
2829
|
);
|
|
2688
2830
|
});
|
|
2689
2831
|
}
|
|
2832
|
+
/**
|
|
2833
|
+
* Mints a signed link that lets an external, unauthenticated actor execute this flow. The flow
|
|
2834
|
+
* must be declared with @externalAccess. Each time the link is opened a fresh flow run is created;
|
|
2835
|
+
* a reusable link can be opened many times, otherwise it permits exactly one run.
|
|
2836
|
+
* @param {Object} [options] Signed link options
|
|
2837
|
+
* @param {Object} [options.inputs] Default inputs applied to every run created from the link
|
|
2838
|
+
* @param {Date} [options.expiresAt] When the link stops being valid
|
|
2839
|
+
* @param {boolean} [options.reusable] Whether the link can be opened more than once
|
|
2840
|
+
* @returns {Promise<{url: string, expiresAt: Date|null, flow: {name: string, runId: string|null}}>} The signed link
|
|
2841
|
+
*/
|
|
2842
|
+
async signedLink(options = {}) {
|
|
2843
|
+
const name = spanNameForModelAPI(this._flowName, "signedLink");
|
|
2844
|
+
return withSpan(name, async () => {
|
|
2845
|
+
const apiUrl = getApiUrl2();
|
|
2846
|
+
const url = `${apiUrl}/flows/json/${encodeURIComponent(
|
|
2847
|
+
this._flowName
|
|
2848
|
+
)}/share`;
|
|
2849
|
+
const body = {};
|
|
2850
|
+
if (options.inputs !== void 0) body.inputs = options.inputs;
|
|
2851
|
+
if (options.reusable !== void 0) body.reusable = options.reusable;
|
|
2852
|
+
if (options.expiresAt !== void 0 && options.expiresAt !== null) {
|
|
2853
|
+
body.expiresAt = options.expiresAt instanceof Date ? options.expiresAt.toISOString() : options.expiresAt;
|
|
2854
|
+
}
|
|
2855
|
+
const response = await fetch(url, {
|
|
2856
|
+
method: "POST",
|
|
2857
|
+
headers: buildHeaders2(this._identity, this._authToken),
|
|
2858
|
+
body: JSON.stringify(body)
|
|
2859
|
+
});
|
|
2860
|
+
if (!response.ok) {
|
|
2861
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
2862
|
+
throw new Error(
|
|
2863
|
+
`Failed to create signed link: ${response.status} ${response.statusText} - ${errorBody.message || JSON.stringify(errorBody)}`
|
|
2864
|
+
);
|
|
2865
|
+
}
|
|
2866
|
+
const result = await response.json();
|
|
2867
|
+
return {
|
|
2868
|
+
url: result.url,
|
|
2869
|
+
expiresAt: result.expiresAt ? new Date(result.expiresAt) : null,
|
|
2870
|
+
flow: {
|
|
2871
|
+
name: result.flow?.name ?? this._flowName,
|
|
2872
|
+
runId: result.flow?.runId ?? null
|
|
2873
|
+
}
|
|
2874
|
+
};
|
|
2875
|
+
});
|
|
2876
|
+
}
|
|
2690
2877
|
/**
|
|
2691
2878
|
* Gets a flow run by ID.
|
|
2692
2879
|
* @param {string} runId The flow run ID
|
|
@@ -2768,6 +2955,61 @@ var FlowsAPI = class _FlowsAPI {
|
|
|
2768
2955
|
}
|
|
2769
2956
|
};
|
|
2770
2957
|
|
|
2958
|
+
// src/integrationServer.js
|
|
2959
|
+
var import_jsonwebtoken3 = __toESM(require("jsonwebtoken"), 1);
|
|
2960
|
+
function buildHeaders3(identity) {
|
|
2961
|
+
const headers = { "Content-Type": "application/json" };
|
|
2962
|
+
const base64pk = process.env.KEEL_PRIVATE_KEY;
|
|
2963
|
+
if (!base64pk) {
|
|
2964
|
+
throw new Error(
|
|
2965
|
+
"KEEL_PRIVATE_KEY is not set; cannot sign the integration proxy token"
|
|
2966
|
+
);
|
|
2967
|
+
}
|
|
2968
|
+
const privateKey = Buffer.from(base64pk, "base64").toString("utf8");
|
|
2969
|
+
const subject = identity && identity.id ? identity.id : "integration-proxy";
|
|
2970
|
+
headers["Authorization"] = "Bearer " + import_jsonwebtoken3.default.sign({}, privateKey, {
|
|
2971
|
+
algorithm: "RS256",
|
|
2972
|
+
expiresIn: 60 * 60 * 24,
|
|
2973
|
+
subject,
|
|
2974
|
+
// Scope the token to the integration proxy so it can't be mistaken for an ordinary user
|
|
2975
|
+
// access token; the runtime proxy verifies this audience before injecting credentials.
|
|
2976
|
+
audience: "integration-proxy",
|
|
2977
|
+
issuer: "https://keel.so"
|
|
2978
|
+
});
|
|
2979
|
+
return headers;
|
|
2980
|
+
}
|
|
2981
|
+
__name(buildHeaders3, "buildHeaders");
|
|
2982
|
+
function getApiUrl3() {
|
|
2983
|
+
const apiUrl = process.env.KEEL_API_URL;
|
|
2984
|
+
if (!apiUrl) {
|
|
2985
|
+
throw new Error("KEEL_API_URL environment variable is not set");
|
|
2986
|
+
}
|
|
2987
|
+
return apiUrl;
|
|
2988
|
+
}
|
|
2989
|
+
__name(getApiUrl3, "getApiUrl");
|
|
2990
|
+
function createIntegrationServer(name, identity) {
|
|
2991
|
+
return {
|
|
2992
|
+
do: /* @__PURE__ */ __name(async (request) => {
|
|
2993
|
+
const url = `${getApiUrl3()}/integrations/${encodeURIComponent(
|
|
2994
|
+
name
|
|
2995
|
+
)}/proxy`;
|
|
2996
|
+
const response = await fetch(url, {
|
|
2997
|
+
method: "POST",
|
|
2998
|
+
headers: buildHeaders3(identity ?? null),
|
|
2999
|
+
body: JSON.stringify(request ?? {})
|
|
3000
|
+
});
|
|
3001
|
+
if (!response.ok) {
|
|
3002
|
+
const text = await response.text();
|
|
3003
|
+
throw new Error(
|
|
3004
|
+
`integration "${name}" proxy request failed (${response.status}): ${text}`
|
|
3005
|
+
);
|
|
3006
|
+
}
|
|
3007
|
+
return await response.json();
|
|
3008
|
+
}, "do")
|
|
3009
|
+
};
|
|
3010
|
+
}
|
|
3011
|
+
__name(createIntegrationServer, "createIntegrationServer");
|
|
3012
|
+
|
|
2771
3013
|
// src/RequestHeaders.ts
|
|
2772
3014
|
var RequestHeaders = class {
|
|
2773
3015
|
/**
|
|
@@ -3948,6 +4190,7 @@ var datePickerInput = /* @__PURE__ */ __name((name, options) => {
|
|
|
3948
4190
|
}, "datePickerInput");
|
|
3949
4191
|
|
|
3950
4192
|
// src/flows/ui/elements/input/file.ts
|
|
4193
|
+
init_File();
|
|
3951
4194
|
var fileInput = /* @__PURE__ */ __name((name, options) => {
|
|
3952
4195
|
return {
|
|
3953
4196
|
__type: "input",
|
|
@@ -3973,6 +4216,7 @@ var fileInput = /* @__PURE__ */ __name((name, options) => {
|
|
|
3973
4216
|
}, "fileInput");
|
|
3974
4217
|
|
|
3975
4218
|
// src/flows/ui/elements/input/imageCapture.ts
|
|
4219
|
+
init_File();
|
|
3976
4220
|
var isMultiOptions = /* @__PURE__ */ __name((opts) => opts && opts.mode === "multi", "isMultiOptions");
|
|
3977
4221
|
function validateEntry(entry, requireCaption, context7) {
|
|
3978
4222
|
if (!entry?.file?.key) {
|
|
@@ -4205,6 +4449,228 @@ var file = /* @__PURE__ */ __name(async (options) => {
|
|
|
4205
4449
|
}, "file");
|
|
4206
4450
|
|
|
4207
4451
|
// src/flows/index.ts
|
|
4452
|
+
var import_kysely6 = require("kysely");
|
|
4453
|
+
|
|
4454
|
+
// src/agents/engine.ts
|
|
4455
|
+
var ENGINE = Symbol.for("keel.agents.engine");
|
|
4456
|
+
var ScriptedEngine = class {
|
|
4457
|
+
constructor(script) {
|
|
4458
|
+
this.script = script;
|
|
4459
|
+
this._requests = [];
|
|
4460
|
+
this.index = 0;
|
|
4461
|
+
}
|
|
4462
|
+
static {
|
|
4463
|
+
__name(this, "ScriptedEngine");
|
|
4464
|
+
}
|
|
4465
|
+
get requests() {
|
|
4466
|
+
return this._requests;
|
|
4467
|
+
}
|
|
4468
|
+
async turn(req) {
|
|
4469
|
+
this._requests.push(req);
|
|
4470
|
+
if (this.index >= this.script.length) {
|
|
4471
|
+
throw new Error(
|
|
4472
|
+
`ScriptedEngine: no scripted result for turn ${this.index + 1}`
|
|
4473
|
+
);
|
|
4474
|
+
}
|
|
4475
|
+
const entry = this.script[this.index++];
|
|
4476
|
+
return typeof entry === "function" ? entry(req) : entry;
|
|
4477
|
+
}
|
|
4478
|
+
};
|
|
4479
|
+
var callCounter = 0;
|
|
4480
|
+
var scriptedTurn = {
|
|
4481
|
+
text(text) {
|
|
4482
|
+
return {
|
|
4483
|
+
text,
|
|
4484
|
+
toolCalls: [],
|
|
4485
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
4486
|
+
stopReason: "end_turn"
|
|
4487
|
+
};
|
|
4488
|
+
},
|
|
4489
|
+
/** Auto-generated ids increment per process — pass an explicit `id` when asserting on ids in tests. */
|
|
4490
|
+
toolCall(name, args, id) {
|
|
4491
|
+
return {
|
|
4492
|
+
text: "",
|
|
4493
|
+
toolCalls: [
|
|
4494
|
+
{ id: id ?? `call_${name}_${++callCounter}`, name, arguments: args }
|
|
4495
|
+
],
|
|
4496
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
4497
|
+
stopReason: "tool_calls"
|
|
4498
|
+
};
|
|
4499
|
+
}
|
|
4500
|
+
};
|
|
4501
|
+
|
|
4502
|
+
// src/agents/engineResolver.ts
|
|
4503
|
+
var import_node_fs2 = require("fs");
|
|
4504
|
+
|
|
4505
|
+
// src/agents/aiSdkEngine.ts
|
|
4506
|
+
var import_ai = require("ai");
|
|
4507
|
+
var import_anthropic = require("@ai-sdk/anthropic");
|
|
4508
|
+
var AiSdkEngine = class {
|
|
4509
|
+
constructor(opts) {
|
|
4510
|
+
this.opts = opts;
|
|
4511
|
+
}
|
|
4512
|
+
static {
|
|
4513
|
+
__name(this, "AiSdkEngine");
|
|
4514
|
+
}
|
|
4515
|
+
// Exposed as a method (not arrow function) so tests can monkey-patch at the
|
|
4516
|
+
// instance level: (engine as any).resolveModel = () => mockModel(...)
|
|
4517
|
+
resolveModel(model) {
|
|
4518
|
+
const slash = model.indexOf("/");
|
|
4519
|
+
const provider = slash === -1 ? "anthropic" : model.slice(0, slash);
|
|
4520
|
+
const id = slash === -1 ? model : model.slice(slash + 1);
|
|
4521
|
+
if (provider !== "anthropic") {
|
|
4522
|
+
throw new Error(
|
|
4523
|
+
`Unsupported provider "${provider}" in model "${model}" \u2014 only anthropic/* is supported in this milestone`
|
|
4524
|
+
);
|
|
4525
|
+
}
|
|
4526
|
+
const baseURL = this.opts.baseURL ?? "https://api.anthropic.com/v1";
|
|
4527
|
+
if (this.opts.authToken) {
|
|
4528
|
+
return (0, import_anthropic.createAnthropic)({
|
|
4529
|
+
apiKey: "unused",
|
|
4530
|
+
baseURL,
|
|
4531
|
+
headers: {
|
|
4532
|
+
"x-api-key": "",
|
|
4533
|
+
authorization: `Bearer ${this.opts.authToken}`,
|
|
4534
|
+
"anthropic-beta": "oauth-2025-04-20"
|
|
4535
|
+
}
|
|
4536
|
+
})(id);
|
|
4537
|
+
}
|
|
4538
|
+
if (!this.opts.apiKey) {
|
|
4539
|
+
throw new Error("AiSdkEngine requires an apiKey or authToken");
|
|
4540
|
+
}
|
|
4541
|
+
return (0, import_anthropic.createAnthropic)({
|
|
4542
|
+
apiKey: this.opts.apiKey,
|
|
4543
|
+
baseURL
|
|
4544
|
+
})(id);
|
|
4545
|
+
}
|
|
4546
|
+
async turn(req) {
|
|
4547
|
+
const model = this.resolveModel(req.model);
|
|
4548
|
+
const tools = {};
|
|
4549
|
+
for (const spec of req.tools ?? []) {
|
|
4550
|
+
tools[spec.name] = (0, import_ai.tool)({
|
|
4551
|
+
description: spec.description,
|
|
4552
|
+
inputSchema: (0, import_ai.jsonSchema)(
|
|
4553
|
+
spec.parameters
|
|
4554
|
+
)
|
|
4555
|
+
});
|
|
4556
|
+
}
|
|
4557
|
+
let toolChoice;
|
|
4558
|
+
if (req.toolChoice === "auto") {
|
|
4559
|
+
toolChoice = "auto";
|
|
4560
|
+
} else if (req.toolChoice && typeof req.toolChoice === "object") {
|
|
4561
|
+
toolChoice = { type: "tool", toolName: req.toolChoice.name };
|
|
4562
|
+
}
|
|
4563
|
+
const messages = req.messages.map((msg) => {
|
|
4564
|
+
if (msg.role === "user") {
|
|
4565
|
+
return { role: "user", content: msg.content };
|
|
4566
|
+
}
|
|
4567
|
+
if (msg.role === "assistant") {
|
|
4568
|
+
const content = [];
|
|
4569
|
+
if (msg.content) {
|
|
4570
|
+
content.push({ type: "text", text: msg.content });
|
|
4571
|
+
}
|
|
4572
|
+
for (const tc of msg.toolCalls ?? []) {
|
|
4573
|
+
content.push({
|
|
4574
|
+
type: "tool-call",
|
|
4575
|
+
toolCallId: tc.id,
|
|
4576
|
+
toolName: tc.name,
|
|
4577
|
+
input: tc.arguments
|
|
4578
|
+
});
|
|
4579
|
+
}
|
|
4580
|
+
return { role: "assistant", content };
|
|
4581
|
+
}
|
|
4582
|
+
return {
|
|
4583
|
+
role: "tool",
|
|
4584
|
+
content: [
|
|
4585
|
+
{
|
|
4586
|
+
type: "tool-result",
|
|
4587
|
+
toolCallId: msg.toolCallId,
|
|
4588
|
+
toolName: msg.toolName,
|
|
4589
|
+
output: { type: "text", value: msg.result }
|
|
4590
|
+
}
|
|
4591
|
+
]
|
|
4592
|
+
};
|
|
4593
|
+
});
|
|
4594
|
+
let res;
|
|
4595
|
+
try {
|
|
4596
|
+
res = await (0, import_ai.generateText)({
|
|
4597
|
+
model,
|
|
4598
|
+
system: req.system,
|
|
4599
|
+
messages,
|
|
4600
|
+
tools: Object.keys(tools).length > 0 ? tools : void 0,
|
|
4601
|
+
toolChoice,
|
|
4602
|
+
stopWhen: (0, import_ai.stepCountIs)(1)
|
|
4603
|
+
});
|
|
4604
|
+
} catch (e) {
|
|
4605
|
+
throw new Error(describeAiError(e));
|
|
4606
|
+
}
|
|
4607
|
+
const toolCalls = res.toolCalls.map((tc) => ({
|
|
4608
|
+
id: tc.toolCallId,
|
|
4609
|
+
name: tc.toolName,
|
|
4610
|
+
arguments: tc.input
|
|
4611
|
+
}));
|
|
4612
|
+
return {
|
|
4613
|
+
text: res.text,
|
|
4614
|
+
toolCalls,
|
|
4615
|
+
usage: {
|
|
4616
|
+
inputTokens: res.usage.inputTokens ?? 0,
|
|
4617
|
+
outputTokens: res.usage.outputTokens ?? 0
|
|
4618
|
+
},
|
|
4619
|
+
stopReason: toolCalls.length > 0 ? "tool_calls" : "end_turn"
|
|
4620
|
+
};
|
|
4621
|
+
}
|
|
4622
|
+
};
|
|
4623
|
+
function describeAiError(e) {
|
|
4624
|
+
const errors = e?.errors;
|
|
4625
|
+
const last = errors && errors.length > 0 ? errors[errors.length - 1] : e;
|
|
4626
|
+
const err = last;
|
|
4627
|
+
let msg = "LLM request failed";
|
|
4628
|
+
if (err?.statusCode) msg += ` (HTTP ${err.statusCode})`;
|
|
4629
|
+
const detail = err?.responseBody?.slice(0, 300) || (err?.message && err.message !== "Error" ? err.message : void 0);
|
|
4630
|
+
if (detail) msg += `: ${detail}`;
|
|
4631
|
+
if (err?.url) msg += ` [${err.url}]`;
|
|
4632
|
+
return msg;
|
|
4633
|
+
}
|
|
4634
|
+
__name(describeAiError, "describeAiError");
|
|
4635
|
+
|
|
4636
|
+
// src/agents/engineResolver.ts
|
|
4637
|
+
var _fileScriptedEngine;
|
|
4638
|
+
function resolveEngine(injected, secrets) {
|
|
4639
|
+
if (injected) return injected;
|
|
4640
|
+
const scriptPath = process.env.FAKE_LLM_SCRIPT;
|
|
4641
|
+
if (scriptPath) {
|
|
4642
|
+
if (!_fileScriptedEngine || _fileScriptedEngine.path !== scriptPath) {
|
|
4643
|
+
const script = JSON.parse((0, import_node_fs2.readFileSync)(scriptPath, "utf8"));
|
|
4644
|
+
_fileScriptedEngine = {
|
|
4645
|
+
path: scriptPath,
|
|
4646
|
+
engine: new ScriptedEngine(script)
|
|
4647
|
+
};
|
|
4648
|
+
}
|
|
4649
|
+
return _fileScriptedEngine.engine;
|
|
4650
|
+
}
|
|
4651
|
+
const key = secrets?.["ANTHROPIC_API_KEY"];
|
|
4652
|
+
if (!key) {
|
|
4653
|
+
throw new Error(
|
|
4654
|
+
"No LLM engine available \u2014 set the ANTHROPIC_API_KEY secret, or inject an engine in tests"
|
|
4655
|
+
);
|
|
4656
|
+
}
|
|
4657
|
+
if (key.startsWith("sk-ant-oat")) {
|
|
4658
|
+
return new AiSdkEngine({ authToken: key });
|
|
4659
|
+
}
|
|
4660
|
+
return new AiSdkEngine({ apiKey: key });
|
|
4661
|
+
}
|
|
4662
|
+
__name(resolveEngine, "resolveEngine");
|
|
4663
|
+
function resolveEngineForContext(context7) {
|
|
4664
|
+
const get = context7?.[ENGINE];
|
|
4665
|
+
if (typeof get !== "function") {
|
|
4666
|
+
throw new Error("An LLM engine is only available within a flow context");
|
|
4667
|
+
}
|
|
4668
|
+
return get();
|
|
4669
|
+
}
|
|
4670
|
+
__name(resolveEngineForContext, "resolveEngineForContext");
|
|
4671
|
+
|
|
4672
|
+
// src/flows/index.ts
|
|
4673
|
+
var import_ksuid2 = __toESM(require("ksuid"), 1);
|
|
4208
4674
|
var STEP_STATUS = /* @__PURE__ */ ((STEP_STATUS2) => {
|
|
4209
4675
|
STEP_STATUS2["NEW"] = "NEW";
|
|
4210
4676
|
STEP_STATUS2["RUNNING"] = "RUNNING";
|
|
@@ -4241,28 +4707,30 @@ var defaultOpts = {
|
|
|
4241
4707
|
async function insertNewStep(db, runId, name, stage) {
|
|
4242
4708
|
await db.transaction().execute(async (trx) => {
|
|
4243
4709
|
await trx.selectFrom("keel.flow_run").select("id").where("id", "=", runId).forUpdate().executeTakeFirst();
|
|
4244
|
-
const existing = await trx.selectFrom("keel.flow_step").select("id").where("run_id", "=", runId).where("name", "=", name).where("status", "
|
|
4710
|
+
const existing = await trx.selectFrom("keel.flow_step").select("id").where("run_id", "=", runId).where("name", "=", name).where("status", "in", ["NEW" /* NEW */, "RUNNING" /* RUNNING */]).executeTakeFirst();
|
|
4245
4711
|
if (existing) {
|
|
4246
4712
|
return;
|
|
4247
4713
|
}
|
|
4248
4714
|
await trx.insertInto("keel.flow_step").values({
|
|
4715
|
+
id: import_ksuid2.default.randomSync().string,
|
|
4249
4716
|
run_id: runId,
|
|
4250
4717
|
name,
|
|
4251
4718
|
stage,
|
|
4252
4719
|
status: "NEW" /* NEW */,
|
|
4253
4720
|
type: "FUNCTION" /* FUNCTION */
|
|
4254
|
-
}).
|
|
4721
|
+
}).returningAll().executeTakeFirst();
|
|
4255
4722
|
});
|
|
4256
4723
|
}
|
|
4257
4724
|
__name(insertNewStep, "insertNewStep");
|
|
4258
4725
|
function createFlowContext(runId, data, action, callback, element, spanId, ctx) {
|
|
4259
4726
|
const usedNames = /* @__PURE__ */ new Set();
|
|
4260
|
-
|
|
4727
|
+
const context7 = {
|
|
4261
4728
|
identity: ctx.identity,
|
|
4262
4729
|
env: ctx.env,
|
|
4263
4730
|
now: ctx.now,
|
|
4264
4731
|
secrets: ctx.secrets,
|
|
4265
4732
|
trace: ctx.trace,
|
|
4733
|
+
module: ctx.module,
|
|
4266
4734
|
complete: /* @__PURE__ */ __name((options) => {
|
|
4267
4735
|
return {
|
|
4268
4736
|
__type: "ui.complete",
|
|
@@ -4282,6 +4750,7 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
|
|
|
4282
4750
|
if (usedNames.has(name)) {
|
|
4283
4751
|
span.setAttribute("step.status", "FAILED" /* FAILED */);
|
|
4284
4752
|
await db.insertInto("keel.flow_step").values({
|
|
4753
|
+
id: import_ksuid2.default.randomSync().string,
|
|
4285
4754
|
run_id: runId,
|
|
4286
4755
|
name,
|
|
4287
4756
|
stage: options.stage,
|
|
@@ -4294,7 +4763,7 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
|
|
|
4294
4763
|
throw new Error(`Duplicate step name: ${name}`);
|
|
4295
4764
|
}
|
|
4296
4765
|
usedNames.add(name);
|
|
4297
|
-
const past = await db.selectFrom("keel.flow_step").where("run_id", "=", runId).where("name", "=", name).selectAll().execute();
|
|
4766
|
+
const past = await db.selectFrom("keel.flow_step").where("run_id", "=", runId).where("name", "=", name).selectAll().select(import_kysely6.sql`value::text`.as("valueRaw")).execute();
|
|
4298
4767
|
const newSteps = past.filter(
|
|
4299
4768
|
(step) => step.status === "NEW" /* NEW */
|
|
4300
4769
|
);
|
|
@@ -4321,7 +4790,13 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
|
|
|
4321
4790
|
if (completedSteps.length === 1) {
|
|
4322
4791
|
span.setAttribute(KEEL_INTERNAL_ATTR, KEEL_INTERNAL_CHILDREN);
|
|
4323
4792
|
span.setAttribute("step.status", "COMPLETED" /* COMPLETED */);
|
|
4324
|
-
|
|
4793
|
+
const raw = completedSteps[0].valueRaw;
|
|
4794
|
+
return raw == null ? void 0 : JSON.parse(raw);
|
|
4795
|
+
}
|
|
4796
|
+
if (runningSteps.length >= 1) {
|
|
4797
|
+
span.setAttribute(KEEL_INTERNAL_ATTR, KEEL_INTERNAL_CHILDREN);
|
|
4798
|
+
span.setAttribute("step.status", "RUNNING" /* RUNNING */);
|
|
4799
|
+
throw new StepCreatedDisrupt();
|
|
4325
4800
|
}
|
|
4326
4801
|
if (newSteps.length === 1) {
|
|
4327
4802
|
let result = null;
|
|
@@ -4347,7 +4822,14 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
|
|
|
4347
4822
|
endTime: /* @__PURE__ */ new Date(),
|
|
4348
4823
|
error: e instanceof Error ? e.message : "An error occurred"
|
|
4349
4824
|
}).where("id", "=", newSteps[0].id).returningAll().executeTakeFirst();
|
|
4350
|
-
if (
|
|
4825
|
+
if (e instanceof NonRetriableError) {
|
|
4826
|
+
span.setAttribute("step.status", "FAILED" /* FAILED */);
|
|
4827
|
+
if (options.onFailure) {
|
|
4828
|
+
await options.onFailure();
|
|
4829
|
+
}
|
|
4830
|
+
throw e;
|
|
4831
|
+
}
|
|
4832
|
+
if (failedSteps.length >= options.retries) {
|
|
4351
4833
|
span.setAttribute("step.status", "FAILED" /* FAILED */);
|
|
4352
4834
|
if (options.onFailure) {
|
|
4353
4835
|
await options.onFailure();
|
|
@@ -4371,11 +4853,6 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
|
|
|
4371
4853
|
span.setAttribute("step.status", "COMPLETED" /* COMPLETED */);
|
|
4372
4854
|
return result;
|
|
4373
4855
|
}
|
|
4374
|
-
if (runningSteps.length >= 1) {
|
|
4375
|
-
span.setAttribute(KEEL_INTERNAL_ATTR, KEEL_INTERNAL_CHILDREN);
|
|
4376
|
-
span.setAttribute("step.status", "RUNNING" /* RUNNING */);
|
|
4377
|
-
throw new StepCreatedDisrupt();
|
|
4378
|
-
}
|
|
4379
4856
|
await insertNewStep(db, runId, name, options.stage);
|
|
4380
4857
|
span.setAttribute(KEEL_INTERNAL_ATTR, KEEL_INTERNAL_CHILDREN);
|
|
4381
4858
|
span.setAttribute("step.status", "NEW" /* NEW */);
|
|
@@ -4394,6 +4871,7 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
|
|
|
4394
4871
|
if (usedNames.has(name)) {
|
|
4395
4872
|
span.setAttribute("step.status", "FAILED" /* FAILED */);
|
|
4396
4873
|
await db.insertInto("keel.flow_step").values({
|
|
4874
|
+
id: import_ksuid2.default.randomSync().string,
|
|
4397
4875
|
run_id: runId,
|
|
4398
4876
|
name,
|
|
4399
4877
|
stage: options.stage,
|
|
@@ -4408,11 +4886,12 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
|
|
|
4408
4886
|
usedNames.add(name);
|
|
4409
4887
|
const { step, inserted } = await db.transaction().execute(async (trx) => {
|
|
4410
4888
|
await trx.selectFrom("keel.flow_run").select("id").where("id", "=", runId).forUpdate().executeTakeFirst();
|
|
4411
|
-
const existing = await trx.selectFrom("keel.flow_step").where("run_id", "=", runId).where("name", "=", name).selectAll().executeTakeFirst();
|
|
4889
|
+
const existing = await trx.selectFrom("keel.flow_step").where("run_id", "=", runId).where("name", "=", name).selectAll().select(import_kysely6.sql`value::text`.as("valueRaw")).executeTakeFirst();
|
|
4412
4890
|
if (existing) {
|
|
4413
4891
|
return { step: existing, inserted: false };
|
|
4414
4892
|
}
|
|
4415
4893
|
const created = await trx.insertInto("keel.flow_step").values({
|
|
4894
|
+
id: import_ksuid2.default.randomSync().string,
|
|
4416
4895
|
run_id: runId,
|
|
4417
4896
|
name,
|
|
4418
4897
|
stage: options.stage,
|
|
@@ -4425,9 +4904,11 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
|
|
|
4425
4904
|
if (step && step.status === "COMPLETED" /* COMPLETED */) {
|
|
4426
4905
|
span.setAttribute(KEEL_INTERNAL_ATTR, KEEL_INTERNAL_CHILDREN);
|
|
4427
4906
|
span.setAttribute("step.status", "COMPLETED" /* COMPLETED */);
|
|
4907
|
+
const rawValue = step.valueRaw;
|
|
4908
|
+
const storedData = rawValue == null ? null : JSON.parse(rawValue);
|
|
4428
4909
|
const parsedData2 = await applyElementGetData(
|
|
4429
4910
|
options.content,
|
|
4430
|
-
transformRichDataTypes(
|
|
4911
|
+
transformRichDataTypes(storedData)
|
|
4431
4912
|
);
|
|
4432
4913
|
if (step.action) {
|
|
4433
4914
|
return { data: parsedData2, action: step.action };
|
|
@@ -4543,6 +5024,8 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
|
|
|
4543
5024
|
}
|
|
4544
5025
|
}
|
|
4545
5026
|
};
|
|
5027
|
+
context7[ENGINE] = () => resolveEngine(ctx.engine, ctx.secrets);
|
|
5028
|
+
return context7;
|
|
4546
5029
|
}
|
|
4547
5030
|
__name(createFlowContext, "createFlowContext");
|
|
4548
5031
|
function wait(milliseconds) {
|
|
@@ -4749,12 +5232,542 @@ async function handleFlow(request, config) {
|
|
|
4749
5232
|
__name(handleFlow, "handleFlow");
|
|
4750
5233
|
|
|
4751
5234
|
// src/index.ts
|
|
4752
|
-
var
|
|
5235
|
+
var import_ksuid4 = __toESM(require("ksuid"), 1);
|
|
5236
|
+
init_File();
|
|
5237
|
+
|
|
5238
|
+
// src/notifications/email-template.tsx
|
|
5239
|
+
var import_components = require("@react-email/components");
|
|
5240
|
+
var import_render = require("@react-email/render");
|
|
5241
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
5242
|
+
function renderBody(text) {
|
|
5243
|
+
return text.split("\n").flatMap((line, i) => i === 0 ? [line] : [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("br", {}, `br-${i}`), line]);
|
|
5244
|
+
}
|
|
5245
|
+
__name(renderBody, "renderBody");
|
|
5246
|
+
var StockEmailTemplate = /* @__PURE__ */ __name(({ content }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_components.Html, { children: [
|
|
5247
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_components.Head, {}),
|
|
5248
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
5249
|
+
import_components.Body,
|
|
5250
|
+
{
|
|
5251
|
+
style: {
|
|
5252
|
+
backgroundColor: "#f6f6f6",
|
|
5253
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
|
|
5254
|
+
},
|
|
5255
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
5256
|
+
import_components.Container,
|
|
5257
|
+
{
|
|
5258
|
+
style: {
|
|
5259
|
+
backgroundColor: "#ffffff",
|
|
5260
|
+
margin: "40px auto",
|
|
5261
|
+
padding: "40px",
|
|
5262
|
+
maxWidth: "560px",
|
|
5263
|
+
borderRadius: "8px"
|
|
5264
|
+
},
|
|
5265
|
+
children: [
|
|
5266
|
+
content.title && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
5267
|
+
import_components.Heading,
|
|
5268
|
+
{
|
|
5269
|
+
style: {
|
|
5270
|
+
fontSize: "22px",
|
|
5271
|
+
fontWeight: 600,
|
|
5272
|
+
color: "#111827",
|
|
5273
|
+
margin: "0 0 16px"
|
|
5274
|
+
},
|
|
5275
|
+
children: content.title
|
|
5276
|
+
}
|
|
5277
|
+
),
|
|
5278
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
5279
|
+
import_components.Text,
|
|
5280
|
+
{
|
|
5281
|
+
style: {
|
|
5282
|
+
fontSize: "15px",
|
|
5283
|
+
lineHeight: "1.6",
|
|
5284
|
+
color: "#374151",
|
|
5285
|
+
margin: "0 0 24px"
|
|
5286
|
+
},
|
|
5287
|
+
children: renderBody(content.body)
|
|
5288
|
+
}
|
|
5289
|
+
),
|
|
5290
|
+
(content.actions ?? []).length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_components.Section, { style: { margin: "0 0 12px" }, children: content.actions.map((action, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
5291
|
+
import_components.Button,
|
|
5292
|
+
{
|
|
5293
|
+
href: action.url,
|
|
5294
|
+
style: {
|
|
5295
|
+
backgroundColor: "#111827",
|
|
5296
|
+
color: "#ffffff",
|
|
5297
|
+
padding: "12px 24px",
|
|
5298
|
+
borderRadius: "6px",
|
|
5299
|
+
fontSize: "14px",
|
|
5300
|
+
fontWeight: 600,
|
|
5301
|
+
textDecoration: "none",
|
|
5302
|
+
display: "inline-block",
|
|
5303
|
+
// Lay buttons out side by side; space between adjacent buttons.
|
|
5304
|
+
marginRight: i < content.actions.length - 1 ? "12px" : "0"
|
|
5305
|
+
},
|
|
5306
|
+
children: action.label
|
|
5307
|
+
},
|
|
5308
|
+
i
|
|
5309
|
+
)) })
|
|
5310
|
+
]
|
|
5311
|
+
}
|
|
5312
|
+
)
|
|
5313
|
+
}
|
|
5314
|
+
)
|
|
5315
|
+
] }), "StockEmailTemplate");
|
|
5316
|
+
async function renderEmailHtml(content) {
|
|
5317
|
+
return (0, import_render.render)(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(StockEmailTemplate, { content }), { pretty: false });
|
|
5318
|
+
}
|
|
5319
|
+
__name(renderEmailHtml, "renderEmailHtml");
|
|
5320
|
+
|
|
5321
|
+
// src/notifications/notify.ts
|
|
5322
|
+
var import_ksuid3 = __toESM(require("ksuid"), 1);
|
|
5323
|
+
function getApiUrl4() {
|
|
5324
|
+
const apiUrl = process.env.KEEL_API_URL;
|
|
5325
|
+
if (!apiUrl) {
|
|
5326
|
+
throw new Error("KEEL_API_URL environment variable is not set");
|
|
5327
|
+
}
|
|
5328
|
+
return apiUrl;
|
|
5329
|
+
}
|
|
5330
|
+
__name(getApiUrl4, "getApiUrl");
|
|
5331
|
+
function toArray(v) {
|
|
5332
|
+
if (v === void 0) return [];
|
|
5333
|
+
return Array.isArray(v) ? v : [v];
|
|
5334
|
+
}
|
|
5335
|
+
__name(toArray, "toArray");
|
|
5336
|
+
function normaliseGroup(group) {
|
|
5337
|
+
if (!group) return [];
|
|
5338
|
+
const refs = [];
|
|
5339
|
+
for (const email of toArray(group.emails)) {
|
|
5340
|
+
refs.push({ kind: "email", value: email });
|
|
5341
|
+
}
|
|
5342
|
+
for (const user of toArray(group.users)) {
|
|
5343
|
+
refs.push({
|
|
5344
|
+
kind: "user",
|
|
5345
|
+
value: typeof user === "string" ? user : user.id
|
|
5346
|
+
});
|
|
5347
|
+
}
|
|
5348
|
+
for (const identity of toArray(group.identities)) {
|
|
5349
|
+
refs.push({
|
|
5350
|
+
kind: "identity",
|
|
5351
|
+
value: typeof identity === "string" ? identity : identity.id
|
|
5352
|
+
});
|
|
5353
|
+
}
|
|
5354
|
+
for (const team of toArray(group.teams)) {
|
|
5355
|
+
refs.push({ kind: "team", value: team });
|
|
5356
|
+
}
|
|
5357
|
+
return refs;
|
|
5358
|
+
}
|
|
5359
|
+
__name(normaliseGroup, "normaliseGroup");
|
|
5360
|
+
async function notifyEmail(input) {
|
|
5361
|
+
return withSpan("notify.email", async () => {
|
|
5362
|
+
const id = import_ksuid3.default.randomSync().string;
|
|
5363
|
+
let rendered;
|
|
5364
|
+
let content;
|
|
5365
|
+
if (typeof input.content === "string") {
|
|
5366
|
+
rendered = input.content;
|
|
5367
|
+
content = void 0;
|
|
5368
|
+
} else {
|
|
5369
|
+
rendered = await renderEmailHtml(input.content);
|
|
5370
|
+
content = input.content;
|
|
5371
|
+
}
|
|
5372
|
+
const body = {
|
|
5373
|
+
id,
|
|
5374
|
+
subject: input.subject,
|
|
5375
|
+
rendered,
|
|
5376
|
+
content,
|
|
5377
|
+
recipients: {
|
|
5378
|
+
to: normaliseGroup(input.recipients.to),
|
|
5379
|
+
cc: normaliseGroup(input.recipients.cc),
|
|
5380
|
+
bcc: normaliseGroup(input.recipients.bcc)
|
|
5381
|
+
}
|
|
5382
|
+
};
|
|
5383
|
+
const response = await fetch(`${getApiUrl4()}/notifications/json/email`, {
|
|
5384
|
+
method: "POST",
|
|
5385
|
+
headers: { "Content-Type": "application/json" },
|
|
5386
|
+
body: JSON.stringify(body)
|
|
5387
|
+
});
|
|
5388
|
+
if (!response.ok) {
|
|
5389
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
5390
|
+
throw new Error(
|
|
5391
|
+
`Failed to send notification: ${response.status} ${response.statusText} - ${errorBody.message || JSON.stringify(errorBody)}`
|
|
5392
|
+
);
|
|
5393
|
+
}
|
|
5394
|
+
await response.body?.cancel();
|
|
5395
|
+
return id;
|
|
5396
|
+
});
|
|
5397
|
+
}
|
|
5398
|
+
__name(notifyEmail, "notifyEmail");
|
|
5399
|
+
function createNotifier() {
|
|
5400
|
+
return { email: notifyEmail };
|
|
5401
|
+
}
|
|
5402
|
+
__name(createNotifier, "createNotifier");
|
|
5403
|
+
|
|
5404
|
+
// src/experimental.ts
|
|
5405
|
+
var experimental_exports = {};
|
|
5406
|
+
__export(experimental_exports, {
|
|
5407
|
+
AiSdkEngine: () => AiSdkEngine,
|
|
5408
|
+
LlmFlowStep: () => LlmFlowStep,
|
|
5409
|
+
ScriptedEngine: () => ScriptedEngine,
|
|
5410
|
+
defineAgent: () => defineAgent,
|
|
5411
|
+
defineTool: () => defineTool,
|
|
5412
|
+
scriptedTurn: () => scriptedTurn
|
|
5413
|
+
});
|
|
5414
|
+
|
|
5415
|
+
// src/agents/schema.ts
|
|
5416
|
+
function canonicalStringify(value) {
|
|
5417
|
+
if (value === null || typeof value !== "object") {
|
|
5418
|
+
return JSON.stringify(value);
|
|
5419
|
+
}
|
|
5420
|
+
if (Array.isArray(value)) {
|
|
5421
|
+
return "[" + value.map((v) => v === void 0 ? "null" : canonicalStringify(v)).join(",") + "]";
|
|
5422
|
+
}
|
|
5423
|
+
const sorted = Object.keys(value).sort().filter((k) => value[k] !== void 0).map((k) => JSON.stringify(k) + ":" + canonicalStringify(value[k])).join(",");
|
|
5424
|
+
return "{" + sorted + "}";
|
|
5425
|
+
}
|
|
5426
|
+
__name(canonicalStringify, "canonicalStringify");
|
|
5427
|
+
function canonicalize(value) {
|
|
5428
|
+
if (value === void 0) return value;
|
|
5429
|
+
return JSON.parse(canonicalStringify(value));
|
|
5430
|
+
}
|
|
5431
|
+
__name(canonicalize, "canonicalize");
|
|
5432
|
+
async function validateSchema(schema, value) {
|
|
5433
|
+
let result = schema["~standard"].validate(value);
|
|
5434
|
+
if (result instanceof Promise) result = await result;
|
|
5435
|
+
if (result.issues) {
|
|
5436
|
+
const issues = result.issues.map((i) => {
|
|
5437
|
+
const path = (i.path ?? []).map(
|
|
5438
|
+
(p) => typeof p === "object" && p !== null && "key" in p ? String(p.key) : String(p)
|
|
5439
|
+
).join(".");
|
|
5440
|
+
return path ? `${path}: ${i.message}` : i.message;
|
|
5441
|
+
}).join("; ");
|
|
5442
|
+
return { ok: false, issues };
|
|
5443
|
+
}
|
|
5444
|
+
return { ok: true, value: result.value };
|
|
5445
|
+
}
|
|
5446
|
+
__name(validateSchema, "validateSchema");
|
|
5447
|
+
async function toJsonSchema(schema) {
|
|
5448
|
+
const vendor = schema["~standard"].vendor;
|
|
5449
|
+
if (vendor === "zod") {
|
|
5450
|
+
const zod = await import("zod");
|
|
5451
|
+
const toJSONSchema = zod.toJSONSchema ?? zod.z?.toJSONSchema;
|
|
5452
|
+
if (typeof toJSONSchema !== "function") {
|
|
5453
|
+
throw new Error(
|
|
5454
|
+
"Deriving a JSON schema from a zod schema requires zod v4"
|
|
5455
|
+
);
|
|
5456
|
+
}
|
|
5457
|
+
return toJSONSchema(schema);
|
|
5458
|
+
}
|
|
5459
|
+
throw new Error(
|
|
5460
|
+
`Unsupported schema vendor "${vendor}" \u2014 agent result/parameter schemas currently support Zod v4`
|
|
5461
|
+
);
|
|
5462
|
+
}
|
|
5463
|
+
__name(toJsonSchema, "toJsonSchema");
|
|
5464
|
+
|
|
5465
|
+
// src/agents/defineAgent.ts
|
|
5466
|
+
var AGENT_NAME_RE = /^[A-Za-z][A-Za-z0-9]*$/;
|
|
5467
|
+
var runCounters = /* @__PURE__ */ new WeakMap();
|
|
5468
|
+
function nextRunNumber(ctx, agentName) {
|
|
5469
|
+
let byName = runCounters.get(ctx);
|
|
5470
|
+
if (!byName) {
|
|
5471
|
+
byName = /* @__PURE__ */ new Map();
|
|
5472
|
+
runCounters.set(ctx, byName);
|
|
5473
|
+
}
|
|
5474
|
+
const n = (byName.get(agentName) ?? 0) + 1;
|
|
5475
|
+
byName.set(agentName, n);
|
|
5476
|
+
return n;
|
|
5477
|
+
}
|
|
5478
|
+
__name(nextRunNumber, "nextRunNumber");
|
|
5479
|
+
async function runAgent(config, ctx, inputs) {
|
|
5480
|
+
const engine = resolveEngineForContext(ctx);
|
|
5481
|
+
const runNumber = nextRunNumber(ctx, config.name);
|
|
5482
|
+
const prefix = `${config.name}#${runNumber}`;
|
|
5483
|
+
const promptContext = await config.beforeRun?.(ctx, { inputs }) ?? {};
|
|
5484
|
+
let system = config.instructions;
|
|
5485
|
+
if (config.result) {
|
|
5486
|
+
system += "\n\nWhen the task is complete you MUST call complete_task with the final result.";
|
|
5487
|
+
}
|
|
5488
|
+
const userTools = config.tools ?? [];
|
|
5489
|
+
const toolSpecs = await Promise.all(
|
|
5490
|
+
userTools.map(async (t) => ({
|
|
5491
|
+
name: t.name,
|
|
5492
|
+
description: t.usage ? `${t.description}
|
|
5493
|
+
${t.usage}` : t.description,
|
|
5494
|
+
parameters: await toJsonSchema(t.parameters)
|
|
5495
|
+
}))
|
|
5496
|
+
);
|
|
5497
|
+
if (config.result) {
|
|
5498
|
+
toolSpecs.push({
|
|
5499
|
+
name: "complete_task",
|
|
5500
|
+
description: "Call this when the task is complete, with the final result.",
|
|
5501
|
+
parameters: await toJsonSchema(config.result)
|
|
5502
|
+
});
|
|
5503
|
+
}
|
|
5504
|
+
const toolMap = new Map(
|
|
5505
|
+
userTools.map((t) => [t.name, t])
|
|
5506
|
+
);
|
|
5507
|
+
const messages = [
|
|
5508
|
+
{
|
|
5509
|
+
role: "user",
|
|
5510
|
+
content: config.prompt({ inputs, context: promptContext })
|
|
5511
|
+
}
|
|
5512
|
+
];
|
|
5513
|
+
const maxTurns = config.maxTurns ?? 10;
|
|
5514
|
+
const maxCompletionNudges = 2;
|
|
5515
|
+
let nudges = 0;
|
|
5516
|
+
for (let turn = 1; turn <= maxTurns; turn++) {
|
|
5517
|
+
const res = canonicalize(
|
|
5518
|
+
await ctx.step(
|
|
5519
|
+
`${prefix}/turn-${turn}/llm`,
|
|
5520
|
+
{ retries: 2, timeout: 3e5 },
|
|
5521
|
+
() => engine.turn({
|
|
5522
|
+
model: config.model,
|
|
5523
|
+
system,
|
|
5524
|
+
messages,
|
|
5525
|
+
...toolSpecs.length ? { tools: toolSpecs } : {}
|
|
5526
|
+
})
|
|
5527
|
+
)
|
|
5528
|
+
);
|
|
5529
|
+
messages.push({
|
|
5530
|
+
role: "assistant",
|
|
5531
|
+
content: res.text,
|
|
5532
|
+
toolCalls: res.toolCalls
|
|
5533
|
+
});
|
|
5534
|
+
const toolCalls = res.toolCalls ?? [];
|
|
5535
|
+
const completeCall = toolCalls.find((c) => c.name === "complete_task");
|
|
5536
|
+
const otherCalls = toolCalls.filter((c) => c.name !== "complete_task");
|
|
5537
|
+
if (completeCall && config.result && otherCalls.length === 0) {
|
|
5538
|
+
const v = await validateSchema(config.result, completeCall.arguments);
|
|
5539
|
+
if (v.ok) {
|
|
5540
|
+
await config.afterRun?.(ctx, v.value);
|
|
5541
|
+
return v.value;
|
|
5542
|
+
}
|
|
5543
|
+
messages.push({
|
|
5544
|
+
role: "tool",
|
|
5545
|
+
toolCallId: completeCall.id,
|
|
5546
|
+
toolName: "complete_task",
|
|
5547
|
+
result: `Validation failed: ${v.issues}. Call complete_task again with corrected arguments.`
|
|
5548
|
+
});
|
|
5549
|
+
continue;
|
|
5550
|
+
}
|
|
5551
|
+
if (res.stopReason === "end_turn") {
|
|
5552
|
+
if (!config.result) {
|
|
5553
|
+
await config.afterRun?.(ctx, res.text);
|
|
5554
|
+
return res.text;
|
|
5555
|
+
}
|
|
5556
|
+
if (++nudges > maxCompletionNudges) {
|
|
5557
|
+
throw new Error(
|
|
5558
|
+
`Agent "${config.name}" finished without calling complete_task after ${maxCompletionNudges} reminders`
|
|
5559
|
+
);
|
|
5560
|
+
}
|
|
5561
|
+
messages.push({
|
|
5562
|
+
role: "user",
|
|
5563
|
+
content: "You must call complete_task with the final result."
|
|
5564
|
+
});
|
|
5565
|
+
continue;
|
|
5566
|
+
}
|
|
5567
|
+
if (completeCall) {
|
|
5568
|
+
messages.push({
|
|
5569
|
+
role: "tool",
|
|
5570
|
+
toolCallId: completeCall.id,
|
|
5571
|
+
toolName: "complete_task",
|
|
5572
|
+
result: config.result ? "complete_task must be the only tool call in a turn. The other tools you called this turn have been run \u2014 review their results, then call complete_task on its own." : `Unknown tool "complete_task" \u2014 available tools: ${userTools.map((t) => t.name).join(", ")}`
|
|
5573
|
+
});
|
|
5574
|
+
}
|
|
5575
|
+
for (let i = 0; i < otherCalls.length; i++) {
|
|
5576
|
+
const call = otherCalls[i];
|
|
5577
|
+
const tool2 = toolMap.get(call.name);
|
|
5578
|
+
if (!tool2) {
|
|
5579
|
+
const available = userTools.map((t) => t.name).join(", ");
|
|
5580
|
+
messages.push({
|
|
5581
|
+
role: "tool",
|
|
5582
|
+
toolCallId: call.id,
|
|
5583
|
+
toolName: call.name,
|
|
5584
|
+
result: `Unknown tool "${call.name}" \u2014 available tools: ${available}`
|
|
5585
|
+
});
|
|
5586
|
+
continue;
|
|
5587
|
+
}
|
|
5588
|
+
const approved = await checkApproval(
|
|
5589
|
+
ctx,
|
|
5590
|
+
prefix,
|
|
5591
|
+
turn,
|
|
5592
|
+
i + 1,
|
|
5593
|
+
tool2,
|
|
5594
|
+
call.arguments
|
|
5595
|
+
);
|
|
5596
|
+
if (!approved) {
|
|
5597
|
+
messages.push({
|
|
5598
|
+
role: "tool",
|
|
5599
|
+
toolCallId: call.id,
|
|
5600
|
+
toolName: call.name,
|
|
5601
|
+
result: `The user rejected this tool call. Do not retry it; choose another course of action.`
|
|
5602
|
+
});
|
|
5603
|
+
continue;
|
|
5604
|
+
}
|
|
5605
|
+
const stepResult = canonicalize(
|
|
5606
|
+
await ctx.step(
|
|
5607
|
+
`${prefix}/turn-${turn}/tool-${i + 1}:${call.name}`,
|
|
5608
|
+
{ retries: 2, timeout: 6e4 },
|
|
5609
|
+
async () => {
|
|
5610
|
+
const v = await validateSchema(tool2.parameters, call.arguments);
|
|
5611
|
+
if (!v.ok) {
|
|
5612
|
+
return { ok: false, error: `Invalid arguments: ${v.issues}` };
|
|
5613
|
+
}
|
|
5614
|
+
return { ok: true, value: await tool2.execute(v.value) ?? null };
|
|
5615
|
+
}
|
|
5616
|
+
)
|
|
5617
|
+
);
|
|
5618
|
+
const resultText = stepResult.ok ? canonicalStringify(stepResult.value) : stepResult.error;
|
|
5619
|
+
messages.push({
|
|
5620
|
+
role: "tool",
|
|
5621
|
+
toolCallId: call.id,
|
|
5622
|
+
toolName: call.name,
|
|
5623
|
+
result: resultText
|
|
5624
|
+
});
|
|
5625
|
+
}
|
|
5626
|
+
}
|
|
5627
|
+
throw new Error(`Agent "${config.name}" exceeded maxTurns (${maxTurns})`);
|
|
5628
|
+
}
|
|
5629
|
+
__name(runAgent, "runAgent");
|
|
5630
|
+
async function checkApproval(ctx, prefix, turn, index, tool2, args) {
|
|
5631
|
+
const needsApproval = tool2.approval === true || typeof tool2.approval === "function" && tool2.approval(args);
|
|
5632
|
+
if (!needsApproval) return true;
|
|
5633
|
+
const result = await ctx.ui.page(
|
|
5634
|
+
`${prefix}/turn-${turn}/approve-${index}:${tool2.name}`,
|
|
5635
|
+
{
|
|
5636
|
+
title: `Approve tool call: ${tool2.name}`,
|
|
5637
|
+
content: [
|
|
5638
|
+
ctx.ui.display.markdown({
|
|
5639
|
+
content: `The agent wants to call **${tool2.name}** with:
|
|
5640
|
+
|
|
5641
|
+
\`\`\`json
|
|
5642
|
+
${canonicalStringify(args)}
|
|
5643
|
+
\`\`\``
|
|
5644
|
+
})
|
|
5645
|
+
],
|
|
5646
|
+
actions: [
|
|
5647
|
+
{ label: "Approve", value: "approve" },
|
|
5648
|
+
{ label: "Reject", value: "reject" }
|
|
5649
|
+
]
|
|
5650
|
+
}
|
|
5651
|
+
);
|
|
5652
|
+
return result.action === "approve";
|
|
5653
|
+
}
|
|
5654
|
+
__name(checkApproval, "checkApproval");
|
|
5655
|
+
function defineAgent(config) {
|
|
5656
|
+
if (!AGENT_NAME_RE.test(config.name)) {
|
|
5657
|
+
throw new Error(
|
|
5658
|
+
`Invalid agent name "${config.name}" \u2014 must be alphanumeric PascalCase`
|
|
5659
|
+
);
|
|
5660
|
+
}
|
|
5661
|
+
const names = /* @__PURE__ */ new Set();
|
|
5662
|
+
for (const tool2 of config.tools ?? []) {
|
|
5663
|
+
if (names.has(tool2.name)) {
|
|
5664
|
+
throw new Error(
|
|
5665
|
+
`Duplicate tool "${tool2.name}" on agent "${config.name}"`
|
|
5666
|
+
);
|
|
5667
|
+
}
|
|
5668
|
+
names.add(tool2.name);
|
|
5669
|
+
}
|
|
5670
|
+
return Object.freeze({
|
|
5671
|
+
name: config.name,
|
|
5672
|
+
config,
|
|
5673
|
+
run: /* @__PURE__ */ __name((ctx, inputs) => runAgent(config, ctx, inputs), "run")
|
|
5674
|
+
});
|
|
5675
|
+
}
|
|
5676
|
+
__name(defineAgent, "defineAgent");
|
|
5677
|
+
|
|
5678
|
+
// src/agents/defineTool.ts
|
|
5679
|
+
var NAME_RE = /^[a-z][a-z0-9_]*$/;
|
|
5680
|
+
var RESERVED = /* @__PURE__ */ new Set(["complete_task"]);
|
|
5681
|
+
function defineTool(config) {
|
|
5682
|
+
if (!NAME_RE.test(config.name)) {
|
|
5683
|
+
throw new Error(
|
|
5684
|
+
`Invalid tool name "${config.name}" \u2014 must be snake_case ([a-z][a-z0-9_]*)`
|
|
5685
|
+
);
|
|
5686
|
+
}
|
|
5687
|
+
if (RESERVED.has(config.name)) {
|
|
5688
|
+
throw new Error(`Tool name "${config.name}" is reserved`);
|
|
5689
|
+
}
|
|
5690
|
+
return Object.freeze({ ...config });
|
|
5691
|
+
}
|
|
5692
|
+
__name(defineTool, "defineTool");
|
|
5693
|
+
|
|
5694
|
+
// src/agents/llm.ts
|
|
5695
|
+
async function LlmFlowStep(ctx, name, options) {
|
|
5696
|
+
const stepValue = await ctx.step(
|
|
5697
|
+
name,
|
|
5698
|
+
{ retries: 2, timeout: options.timeout ?? 3e5 },
|
|
5699
|
+
async () => {
|
|
5700
|
+
const engine = resolveEngineForContext(ctx);
|
|
5701
|
+
if (!options.result) {
|
|
5702
|
+
const res = await engine.turn({
|
|
5703
|
+
model: options.model,
|
|
5704
|
+
system: options.system,
|
|
5705
|
+
messages: [{ role: "user", content: options.prompt }]
|
|
5706
|
+
});
|
|
5707
|
+
return res.text;
|
|
5708
|
+
}
|
|
5709
|
+
const parameters = await toJsonSchema(options.result);
|
|
5710
|
+
const messages = [
|
|
5711
|
+
{ role: "user", content: options.prompt }
|
|
5712
|
+
];
|
|
5713
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
5714
|
+
const res = await engine.turn({
|
|
5715
|
+
model: options.model,
|
|
5716
|
+
system: options.system,
|
|
5717
|
+
messages,
|
|
5718
|
+
tools: [
|
|
5719
|
+
{
|
|
5720
|
+
name: "complete_task",
|
|
5721
|
+
description: "Return the final result of the task.",
|
|
5722
|
+
parameters
|
|
5723
|
+
}
|
|
5724
|
+
],
|
|
5725
|
+
toolChoice: { name: "complete_task" }
|
|
5726
|
+
});
|
|
5727
|
+
const call = res.toolCalls.find((c) => c.name === "complete_task");
|
|
5728
|
+
if (call) {
|
|
5729
|
+
const v = await validateSchema(options.result, call.arguments);
|
|
5730
|
+
if (v.ok) return v.value;
|
|
5731
|
+
messages.push(
|
|
5732
|
+
{
|
|
5733
|
+
role: "assistant",
|
|
5734
|
+
content: res.text,
|
|
5735
|
+
toolCalls: res.toolCalls
|
|
5736
|
+
},
|
|
5737
|
+
{
|
|
5738
|
+
role: "tool",
|
|
5739
|
+
toolCallId: call.id,
|
|
5740
|
+
toolName: "complete_task",
|
|
5741
|
+
result: `Validation failed: ${v.issues}. Call complete_task again with corrected arguments.`
|
|
5742
|
+
}
|
|
5743
|
+
);
|
|
5744
|
+
continue;
|
|
5745
|
+
}
|
|
5746
|
+
messages.push(
|
|
5747
|
+
{ role: "assistant", content: res.text },
|
|
5748
|
+
{
|
|
5749
|
+
role: "user",
|
|
5750
|
+
content: "You must call complete_task with the final result."
|
|
5751
|
+
}
|
|
5752
|
+
);
|
|
5753
|
+
}
|
|
5754
|
+
throw new NonRetriableError(
|
|
5755
|
+
`LlmFlowStep("${name}"): result failed validation after 3 attempts`
|
|
5756
|
+
);
|
|
5757
|
+
}
|
|
5758
|
+
);
|
|
5759
|
+
return canonicalize(stepValue);
|
|
5760
|
+
}
|
|
5761
|
+
__name(LlmFlowStep, "LlmFlowStep");
|
|
5762
|
+
|
|
5763
|
+
// src/index.ts
|
|
5764
|
+
var import_zod = require("zod");
|
|
4753
5765
|
var createTraceAPI2 = createTraceAPI;
|
|
4754
5766
|
function ksuid() {
|
|
4755
|
-
return
|
|
5767
|
+
return import_ksuid4.default.randomSync().string;
|
|
4756
5768
|
}
|
|
4757
5769
|
__name(ksuid, "ksuid");
|
|
5770
|
+
var notify = createNotifier();
|
|
4758
5771
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4759
5772
|
0 && (module.exports = {
|
|
4760
5773
|
Duration,
|
|
@@ -4777,14 +5790,20 @@ __name(ksuid, "ksuid");
|
|
|
4777
5790
|
TaskAPI,
|
|
4778
5791
|
checkBuiltInPermissions,
|
|
4779
5792
|
createFlowContext,
|
|
5793
|
+
createIntegrationServer,
|
|
5794
|
+
createNotifier,
|
|
4780
5795
|
createTraceAPI,
|
|
5796
|
+
experimental,
|
|
4781
5797
|
handleFlow,
|
|
4782
5798
|
handleJob,
|
|
4783
5799
|
handleRequest,
|
|
4784
5800
|
handleRoute,
|
|
4785
5801
|
handleSubscriber,
|
|
5802
|
+
insertNewStep,
|
|
4786
5803
|
ksuid,
|
|
5804
|
+
notify,
|
|
4787
5805
|
tracing,
|
|
4788
|
-
useDatabase
|
|
5806
|
+
useDatabase,
|
|
5807
|
+
z
|
|
4789
5808
|
});
|
|
4790
5809
|
//# sourceMappingURL=index.cjs.map
|