@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 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: () => import_ksuid2.default,
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
- return db.transaction().execute(async (transaction) => {
506
- return dbInstance.run(transaction, async () => {
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
- return db.connection().execute(async (sDb) => {
512
- return dbInstance.run(sDb, async () => {
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 sql4 = args[0];
947
+ const sql5 = args[0];
595
948
  let sqlAttribute = false;
596
- let spanName = txStatements[sql4.toLowerCase()];
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 sql4 = args[0];
1017
+ const sql5 = args[0];
665
1018
  let sqlAttribute = false;
666
- let spanName = txStatements[sql4.toLowerCase()];
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 sql4 = "NOW()";
1247
+ let sql5 = "NOW()";
1158
1248
  if (this.offset !== 0) {
1159
- sql4 = `${sql4} + INTERVAL '${this.offset} ${this.period}'`;
1249
+ sql5 = `${sql5} + INTERVAL '${this.offset} ${this.period}'`;
1160
1250
  }
1161
1251
  if (this.complete) {
1162
- sql4 = `DATE_TRUNC('${this.period}', ${sql4})`;
1252
+ sql5 = `DATE_TRUNC('${this.period}', ${sql5})`;
1163
1253
  } else {
1164
- sql4 = `(${sql4})`;
1254
+ sql5 = `(${sql5})`;
1165
1255
  }
1166
- return sql4;
1256
+ return sql5;
1167
1257
  }
1168
1258
  periodEndSQL() {
1169
- let sql4 = this.periodStartSQL();
1259
+ let sql5 = this.periodStartSQL();
1170
1260
  if (this.value != 0) {
1171
- sql4 = `(${sql4} + INTERVAL '${this.value} ${this.period}')`;
1261
+ sql5 = `(${sql5} + INTERVAL '${this.value} ${this.period}')`;
1172
1262
  }
1173
- return sql4;
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
- return transformRichDataTypes(camelCaseObject(row2));
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", "=", "NEW" /* NEW */).executeTakeFirst();
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
- }).execute();
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
- return {
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
- return completedSteps[0].value;
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 (failedSteps.length >= options.retries || e instanceof NonRetriableError) {
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(step.value)
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 import_ksuid2 = __toESM(require("ksuid"), 1);
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 import_ksuid2.default.randomSync().string;
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