@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.js CHANGED
@@ -1,10 +1,336 @@
1
1
  var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
2
3
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
3
7
  var __export = (target, all) => {
4
8
  for (var name in all)
5
9
  __defProp(target, name, { get: all[name], enumerable: true });
6
10
  };
7
11
 
12
+ // src/File.ts
13
+ var File_exports = {};
14
+ __export(File_exports, {
15
+ File: () => File,
16
+ InlineFile: () => InlineFile,
17
+ buildContentDisposition: () => buildContentDisposition,
18
+ deleteStoredFile: () => deleteStoredFile,
19
+ rewriteFilesDomain: () => rewriteFilesDomain
20
+ });
21
+ import {
22
+ S3Client,
23
+ PutObjectCommand,
24
+ GetObjectCommand,
25
+ DeleteObjectCommand
26
+ } from "@aws-sdk/client-s3";
27
+ import { fromEnv } from "@aws-sdk/credential-providers";
28
+ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
29
+ import KSUID from "ksuid";
30
+ function rewriteFilesDomain(url) {
31
+ const domain = process.env.KEEL_FILES_DOMAIN;
32
+ if (domain) {
33
+ const override = new URL(domain);
34
+ url.protocol = override.protocol;
35
+ url.hostname = override.hostname;
36
+ url.port = override.port;
37
+ const overridePath = override.pathname.replace(/\/+$/, "");
38
+ if (overridePath && overridePath !== "/") {
39
+ url.pathname = overridePath + url.pathname;
40
+ }
41
+ }
42
+ return url;
43
+ }
44
+ function encodeRFC5987(value) {
45
+ return encodeURIComponent(value).replace(
46
+ /['()*]/g,
47
+ (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase()
48
+ );
49
+ }
50
+ function buildContentDisposition(disposition, filename) {
51
+ const type = disposition === "attachment" ? "attachment" : "inline";
52
+ if (!filename || filename.trim() === "") {
53
+ return type;
54
+ }
55
+ const asciiFallback = filename.replace(/[^\x20-\x7e]|["\\]/g, "_");
56
+ return `${type}; filename="${asciiFallback}"; filename*=UTF-8''${encodeRFC5987(
57
+ filename
58
+ )}`;
59
+ }
60
+ async function storeFile(contents, key, filename, contentType, size, expires) {
61
+ if (!s3Client) {
62
+ throw new Error("S3 client is required");
63
+ }
64
+ const params = {
65
+ Bucket: process.env.KEEL_FILES_BUCKET_NAME,
66
+ Key: "files/" + key,
67
+ Body: contents,
68
+ ContentType: contentType,
69
+ ContentDisposition: `attachment; filename="${encodeURIComponent(
70
+ filename
71
+ )}"`,
72
+ Metadata: {
73
+ filename
74
+ },
75
+ ACL: "private"
76
+ };
77
+ if (expires) {
78
+ if (expires instanceof Date) {
79
+ params.Expires = expires;
80
+ } else {
81
+ console.warn("Invalid expires value. Skipping Expires parameter.");
82
+ }
83
+ }
84
+ const command = new PutObjectCommand(params);
85
+ try {
86
+ await s3Client.send(command);
87
+ } catch (error) {
88
+ console.error("Error uploading file:", error);
89
+ throw error;
90
+ }
91
+ }
92
+ async function deleteStoredFile(key) {
93
+ if (!s3Client) {
94
+ throw new Error("S3 client is required");
95
+ }
96
+ await s3Client.send(
97
+ new DeleteObjectCommand({
98
+ Bucket: process.env.KEEL_FILES_BUCKET_NAME,
99
+ Key: "files/" + key
100
+ })
101
+ );
102
+ }
103
+ var s3Client, InlineFile, File;
104
+ var init_File = __esm({
105
+ "src/File.ts"() {
106
+ "use strict";
107
+ s3Client = (() => {
108
+ if (!process.env.KEEL_FILES_BUCKET_NAME) {
109
+ return null;
110
+ }
111
+ const endpoint = process.env.KEEL_S3_ENDPOINT;
112
+ if (endpoint) {
113
+ return new S3Client({
114
+ region: process.env.KEEL_REGION,
115
+ credentials: {
116
+ accessKeyId: "keelstorage",
117
+ secretAccessKey: "keelstorage"
118
+ },
119
+ endpoint
120
+ });
121
+ }
122
+ const testEndpoint = process.env.TEST_AWS_ENDPOINT;
123
+ if (testEndpoint) {
124
+ return new S3Client({
125
+ region: process.env.KEEL_REGION,
126
+ credentials: {
127
+ accessKeyId: "test",
128
+ secretAccessKey: "test"
129
+ },
130
+ endpointProvider: /* @__PURE__ */ __name(() => {
131
+ return {
132
+ url: new URL(testEndpoint)
133
+ };
134
+ }, "endpointProvider")
135
+ });
136
+ }
137
+ return new S3Client({
138
+ region: process.env.KEEL_REGION,
139
+ credentials: fromEnv()
140
+ });
141
+ })();
142
+ __name(rewriteFilesDomain, "rewriteFilesDomain");
143
+ __name(encodeRFC5987, "encodeRFC5987");
144
+ __name(buildContentDisposition, "buildContentDisposition");
145
+ InlineFile = class _InlineFile {
146
+ static {
147
+ __name(this, "InlineFile");
148
+ }
149
+ constructor(input) {
150
+ this._filename = input.filename;
151
+ this._contentType = input.contentType;
152
+ this._contents = null;
153
+ }
154
+ static fromDataURL(dataURL) {
155
+ const info = dataURL.split(",")[0].split(":")[1];
156
+ const data = dataURL.split(",")[1];
157
+ const mimeType = info.split(";")[0];
158
+ const name = info.split(";")[1].split("=")[1] || "file";
159
+ const buffer = Buffer.from(data, "base64");
160
+ const file2 = new _InlineFile({ filename: name, contentType: mimeType });
161
+ file2.write(buffer);
162
+ return file2;
163
+ }
164
+ // Gets size of the file's contents in bytes
165
+ get size() {
166
+ if (this._contents) {
167
+ return this._contents.size;
168
+ }
169
+ return 0;
170
+ }
171
+ // Gets the media type of the file contents
172
+ get contentType() {
173
+ return this._contentType;
174
+ }
175
+ // Gets the name of the file
176
+ get filename() {
177
+ return this._filename;
178
+ }
179
+ // Write the files contents from a buffer
180
+ write(buffer) {
181
+ this._contents = new Blob([
182
+ new Uint8Array(
183
+ buffer.buffer,
184
+ buffer.byteOffset,
185
+ buffer.byteLength
186
+ )
187
+ ]);
188
+ }
189
+ // Reads the contents of the file as a buffer
190
+ async read() {
191
+ if (!this._contents) {
192
+ throw new Error("No contents to read");
193
+ }
194
+ const arrayBuffer = await this._contents.arrayBuffer();
195
+ return Buffer.from(arrayBuffer);
196
+ }
197
+ // Persists the file
198
+ async store(expires = null) {
199
+ const content = await this.read();
200
+ const key = KSUID.randomSync().string;
201
+ await storeFile(
202
+ content,
203
+ key,
204
+ this._filename,
205
+ this._contentType,
206
+ this.size,
207
+ expires
208
+ );
209
+ return new File({
210
+ key,
211
+ size: this.size,
212
+ filename: this.filename,
213
+ contentType: this.contentType
214
+ });
215
+ }
216
+ };
217
+ File = class _File extends InlineFile {
218
+ static {
219
+ __name(this, "File");
220
+ }
221
+ constructor(input) {
222
+ super({
223
+ filename: input.filename || "",
224
+ contentType: input.contentType || ""
225
+ });
226
+ this._key = input.key || "";
227
+ this._size = input.size || 0;
228
+ }
229
+ // Creates a new instance from the database record
230
+ static fromDbRecord(input) {
231
+ return new _File({
232
+ key: input.key,
233
+ filename: input.filename,
234
+ size: input.size,
235
+ contentType: input.contentType
236
+ });
237
+ }
238
+ get size() {
239
+ return this._size;
240
+ }
241
+ // Gets the stored key
242
+ get key() {
243
+ return this._key;
244
+ }
245
+ get isPublic() {
246
+ return false;
247
+ }
248
+ async read() {
249
+ if (this._contents) {
250
+ const arrayBuffer = await this._contents.arrayBuffer();
251
+ return Buffer.from(arrayBuffer);
252
+ }
253
+ if (!s3Client) {
254
+ throw new Error("S3 client is required");
255
+ }
256
+ const params = {
257
+ Bucket: process.env.KEEL_FILES_BUCKET_NAME,
258
+ Key: "files/" + this.key
259
+ };
260
+ const command = new GetObjectCommand(params);
261
+ const response = await s3Client.send(command);
262
+ const blob = await response.Body.transformToByteArray();
263
+ return Buffer.from(blob);
264
+ }
265
+ async store(expires = null) {
266
+ if (this._contents) {
267
+ const contents = await this.read();
268
+ await storeFile(
269
+ contents,
270
+ this.key,
271
+ this.filename,
272
+ this.contentType,
273
+ this.size,
274
+ expires
275
+ );
276
+ }
277
+ return this;
278
+ }
279
+ // Generates a presigned download URL.
280
+ //
281
+ // By default the browser previews the file inline and, when saved, uses the
282
+ // file's own filename. Pass `contentDisposition: "attachment"` to force a
283
+ // download, or `filename` to override the suggested name.
284
+ async getPresignedUrl(options) {
285
+ if (!s3Client) {
286
+ throw new Error("S3 client is required");
287
+ }
288
+ const disposition = options?.contentDisposition ?? "inline";
289
+ const filename = options?.filename ?? this.filename;
290
+ const command = new GetObjectCommand({
291
+ Bucket: process.env.KEEL_FILES_BUCKET_NAME,
292
+ Key: "files/" + this.key,
293
+ ResponseContentDisposition: buildContentDisposition(
294
+ disposition,
295
+ filename
296
+ )
297
+ });
298
+ const url = await getSignedUrl(s3Client, command, { expiresIn: 60 * 60 });
299
+ return rewriteFilesDomain(new URL(url));
300
+ }
301
+ // Generates a presigned upload URL. If the file doesn't have a key, a new one will be generated
302
+ async getPresignedUploadUrl() {
303
+ if (!s3Client) {
304
+ throw new Error("S3 client is required");
305
+ }
306
+ if (!this.key) {
307
+ this._key = KSUID.randomSync().string;
308
+ }
309
+ const command = new PutObjectCommand({
310
+ Bucket: process.env.KEEL_FILES_BUCKET_NAME,
311
+ Key: "files/" + this.key
312
+ });
313
+ const url = await getSignedUrl(s3Client, command, { expiresIn: 60 * 60 });
314
+ return rewriteFilesDomain(new URL(url));
315
+ }
316
+ // Persists the file
317
+ toDbRecord() {
318
+ return {
319
+ key: this.key,
320
+ filename: this.filename,
321
+ contentType: this.contentType,
322
+ size: this.size
323
+ };
324
+ }
325
+ toJSON() {
326
+ return this.toDbRecord();
327
+ }
328
+ };
329
+ __name(storeFile, "storeFile");
330
+ __name(deleteStoredFile, "deleteStoredFile");
331
+ }
332
+ });
333
+
8
334
  // src/ModelAPI.js
9
335
  import { sql as sql3 } from "kysely";
10
336
 
@@ -72,6 +398,9 @@ var AuditContextPlugin = class {
72
398
  const rawNode = sql`set_trace_id(${audit.traceId})`.as(this.traceIdAlias).toOperationNode();
73
399
  returning.selections.push(SelectionNode.create(rawNode));
74
400
  }
401
+ if (returning.selections.length === 0) {
402
+ return { ...args.node };
403
+ }
75
404
  return {
76
405
  ...args.node,
77
406
  returning
@@ -441,20 +770,44 @@ var KEEL_INTERNAL_CHILDREN = "includeChildrenSpans";
441
770
  import WebSocket from "ws";
442
771
  import { readFileSync } from "fs";
443
772
  var dbInstance = new AsyncLocalStorage2();
773
+ var fileCleanupStore = new AsyncLocalStorage2();
774
+ function deferFileDeletion(key) {
775
+ fileCleanupStore.getStore()?.add(key);
776
+ }
777
+ __name(deferFileDeletion, "deferFileDeletion");
778
+ async function flushFileDeletions(keys) {
779
+ if (keys.size === 0) {
780
+ return;
781
+ }
782
+ const { deleteStoredFile: deleteStoredFile2 } = await Promise.resolve().then(() => (init_File(), File_exports));
783
+ for (const key of keys) {
784
+ try {
785
+ await deleteStoredFile2(key);
786
+ } catch (e) {
787
+ console.error("failed to delete orphaned file from storage", e);
788
+ }
789
+ }
790
+ }
791
+ __name(flushFileDeletions, "flushFileDeletions");
444
792
  var vitestDb = null;
445
793
  async function withDatabase(db, requiresTransaction, cb) {
794
+ const pending = /* @__PURE__ */ new Set();
446
795
  if (requiresTransaction) {
447
- return db.transaction().execute(async (transaction) => {
448
- return dbInstance.run(transaction, async () => {
449
- return cb({ transaction });
796
+ const result2 = await db.transaction().execute(async (transaction) => {
797
+ return dbInstance.run(transaction, () => {
798
+ return fileCleanupStore.run(pending, () => cb({ transaction }));
450
799
  });
451
800
  });
801
+ await flushFileDeletions(pending);
802
+ return result2;
452
803
  }
453
- return db.connection().execute(async (sDb) => {
454
- return dbInstance.run(sDb, async () => {
455
- return cb({ sDb });
804
+ const result = await db.connection().execute(async (sDb) => {
805
+ return dbInstance.run(sDb, () => {
806
+ return fileCleanupStore.run(pending, () => cb({ sDb }));
456
807
  });
457
808
  });
809
+ await flushFileDeletions(pending);
810
+ return result;
458
811
  }
459
812
  __name(withDatabase, "withDatabase");
460
813
  function useDatabase() {
@@ -533,9 +886,9 @@ var InstrumentedClient = class extends Client {
533
886
  }
534
887
  async query(...args) {
535
888
  const _super = super.query.bind(this);
536
- const sql4 = args[0];
889
+ const sql5 = args[0];
537
890
  let sqlAttribute = false;
538
- let spanName = txStatements[sql4.toLowerCase()];
891
+ let spanName = txStatements[sql5.toLowerCase()];
539
892
  if (!spanName) {
540
893
  spanName = "Database Query";
541
894
  sqlAttribute = true;
@@ -603,9 +956,9 @@ function getDialect(connString) {
603
956
  pool.on("connect", (client) => {
604
957
  const originalQuery = client.query;
605
958
  client.query = function(...args) {
606
- const sql4 = args[0];
959
+ const sql5 = args[0];
607
960
  let sqlAttribute = false;
608
- let spanName = txStatements[sql4.toLowerCase()];
961
+ let spanName = txStatements[sql5.toLowerCase()];
609
962
  if (!spanName) {
610
963
  spanName = "Database Query";
611
964
  sqlAttribute = true;
@@ -629,275 +982,8 @@ function getDialect(connString) {
629
982
  }
630
983
  __name(getDialect, "getDialect");
631
984
 
632
- // src/File.ts
633
- import {
634
- S3Client,
635
- PutObjectCommand,
636
- GetObjectCommand
637
- } from "@aws-sdk/client-s3";
638
- import { fromEnv } from "@aws-sdk/credential-providers";
639
- import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
640
- import KSUID from "ksuid";
641
- var s3Client = (() => {
642
- if (!process.env.KEEL_FILES_BUCKET_NAME) {
643
- return null;
644
- }
645
- const endpoint = process.env.KEEL_S3_ENDPOINT;
646
- if (endpoint) {
647
- return new S3Client({
648
- region: process.env.KEEL_REGION,
649
- credentials: {
650
- accessKeyId: "keelstorage",
651
- secretAccessKey: "keelstorage"
652
- },
653
- endpoint
654
- });
655
- }
656
- const testEndpoint = process.env.TEST_AWS_ENDPOINT;
657
- if (testEndpoint) {
658
- return new S3Client({
659
- region: process.env.KEEL_REGION,
660
- credentials: {
661
- accessKeyId: "test",
662
- secretAccessKey: "test"
663
- },
664
- endpointProvider: /* @__PURE__ */ __name(() => {
665
- return {
666
- url: new URL(testEndpoint)
667
- };
668
- }, "endpointProvider")
669
- });
670
- }
671
- return new S3Client({
672
- region: process.env.KEEL_REGION,
673
- credentials: fromEnv()
674
- });
675
- })();
676
- function rewriteFilesDomain(url) {
677
- const domain = process.env.KEEL_FILES_DOMAIN;
678
- if (domain) {
679
- const override = new URL(domain);
680
- url.protocol = override.protocol;
681
- url.hostname = override.hostname;
682
- url.port = override.port;
683
- const overridePath = override.pathname.replace(/\/+$/, "");
684
- if (overridePath && overridePath !== "/") {
685
- url.pathname = overridePath + url.pathname;
686
- }
687
- }
688
- return url;
689
- }
690
- __name(rewriteFilesDomain, "rewriteFilesDomain");
691
- var InlineFile = class _InlineFile {
692
- static {
693
- __name(this, "InlineFile");
694
- }
695
- constructor(input) {
696
- this._filename = input.filename;
697
- this._contentType = input.contentType;
698
- this._contents = null;
699
- }
700
- static fromDataURL(dataURL) {
701
- const info = dataURL.split(",")[0].split(":")[1];
702
- const data = dataURL.split(",")[1];
703
- const mimeType = info.split(";")[0];
704
- const name = info.split(";")[1].split("=")[1] || "file";
705
- const buffer = Buffer.from(data, "base64");
706
- const file2 = new _InlineFile({ filename: name, contentType: mimeType });
707
- file2.write(buffer);
708
- return file2;
709
- }
710
- // Gets size of the file's contents in bytes
711
- get size() {
712
- if (this._contents) {
713
- return this._contents.size;
714
- }
715
- return 0;
716
- }
717
- // Gets the media type of the file contents
718
- get contentType() {
719
- return this._contentType;
720
- }
721
- // Gets the name of the file
722
- get filename() {
723
- return this._filename;
724
- }
725
- // Write the files contents from a buffer
726
- write(buffer) {
727
- this._contents = new Blob([
728
- new Uint8Array(
729
- buffer.buffer,
730
- buffer.byteOffset,
731
- buffer.byteLength
732
- )
733
- ]);
734
- }
735
- // Reads the contents of the file as a buffer
736
- async read() {
737
- if (!this._contents) {
738
- throw new Error("No contents to read");
739
- }
740
- const arrayBuffer = await this._contents.arrayBuffer();
741
- return Buffer.from(arrayBuffer);
742
- }
743
- // Persists the file
744
- async store(expires = null) {
745
- const content = await this.read();
746
- const key = KSUID.randomSync().string;
747
- await storeFile(
748
- content,
749
- key,
750
- this._filename,
751
- this._contentType,
752
- this.size,
753
- expires
754
- );
755
- return new File({
756
- key,
757
- size: this.size,
758
- filename: this.filename,
759
- contentType: this.contentType
760
- });
761
- }
762
- };
763
- var File = class _File extends InlineFile {
764
- static {
765
- __name(this, "File");
766
- }
767
- constructor(input) {
768
- super({
769
- filename: input.filename || "",
770
- contentType: input.contentType || ""
771
- });
772
- this._key = input.key || "";
773
- this._size = input.size || 0;
774
- }
775
- // Creates a new instance from the database record
776
- static fromDbRecord(input) {
777
- return new _File({
778
- key: input.key,
779
- filename: input.filename,
780
- size: input.size,
781
- contentType: input.contentType
782
- });
783
- }
784
- get size() {
785
- return this._size;
786
- }
787
- // Gets the stored key
788
- get key() {
789
- return this._key;
790
- }
791
- get isPublic() {
792
- return false;
793
- }
794
- async read() {
795
- if (this._contents) {
796
- const arrayBuffer = await this._contents.arrayBuffer();
797
- return Buffer.from(arrayBuffer);
798
- }
799
- if (!s3Client) {
800
- throw new Error("S3 client is required");
801
- }
802
- const params = {
803
- Bucket: process.env.KEEL_FILES_BUCKET_NAME,
804
- Key: "files/" + this.key
805
- };
806
- const command = new GetObjectCommand(params);
807
- const response = await s3Client.send(command);
808
- const blob = await response.Body.transformToByteArray();
809
- return Buffer.from(blob);
810
- }
811
- async store(expires = null) {
812
- if (this._contents) {
813
- const contents = await this.read();
814
- await storeFile(
815
- contents,
816
- this.key,
817
- this.filename,
818
- this.contentType,
819
- this.size,
820
- expires
821
- );
822
- }
823
- return this;
824
- }
825
- // Generates a presigned download URL
826
- async getPresignedUrl() {
827
- if (!s3Client) {
828
- throw new Error("S3 client is required");
829
- }
830
- const command = new GetObjectCommand({
831
- Bucket: process.env.KEEL_FILES_BUCKET_NAME,
832
- Key: "files/" + this.key,
833
- ResponseContentDisposition: "inline"
834
- });
835
- const url = await getSignedUrl(s3Client, command, { expiresIn: 60 * 60 });
836
- return rewriteFilesDomain(new URL(url));
837
- }
838
- // Generates a presigned upload URL. If the file doesn't have a key, a new one will be generated
839
- async getPresignedUploadUrl() {
840
- if (!s3Client) {
841
- throw new Error("S3 client is required");
842
- }
843
- if (!this.key) {
844
- this._key = KSUID.randomSync().string;
845
- }
846
- const command = new PutObjectCommand({
847
- Bucket: process.env.KEEL_FILES_BUCKET_NAME,
848
- Key: "files/" + this.key
849
- });
850
- const url = await getSignedUrl(s3Client, command, { expiresIn: 60 * 60 });
851
- return rewriteFilesDomain(new URL(url));
852
- }
853
- // Persists the file
854
- toDbRecord() {
855
- return {
856
- key: this.key,
857
- filename: this.filename,
858
- contentType: this.contentType,
859
- size: this.size
860
- };
861
- }
862
- toJSON() {
863
- return this.toDbRecord();
864
- }
865
- };
866
- async function storeFile(contents, key, filename, contentType, size, expires) {
867
- if (!s3Client) {
868
- throw new Error("S3 client is required");
869
- }
870
- const params = {
871
- Bucket: process.env.KEEL_FILES_BUCKET_NAME,
872
- Key: "files/" + key,
873
- Body: contents,
874
- ContentType: contentType,
875
- ContentDisposition: `attachment; filename="${encodeURIComponent(
876
- filename
877
- )}"`,
878
- Metadata: {
879
- filename
880
- },
881
- ACL: "private"
882
- };
883
- if (expires) {
884
- if (expires instanceof Date) {
885
- params.Expires = expires;
886
- } else {
887
- console.warn("Invalid expires value. Skipping Expires parameter.");
888
- }
889
- }
890
- const command = new PutObjectCommand(params);
891
- try {
892
- await s3Client.send(command);
893
- } catch (error) {
894
- console.error("Error uploading file:", error);
895
- throw error;
896
- }
897
- }
898
- __name(storeFile, "storeFile");
899
-
900
985
  // src/parsing.js
986
+ init_File();
901
987
  var dateFormat = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
902
988
  function parseInputs(inputs) {
903
989
  if (inputs != null && typeof inputs === "object") {
@@ -1103,23 +1189,23 @@ var TimePeriod = class _TimePeriod {
1103
1189
  return new _TimePeriod(period, value, offset, complete2);
1104
1190
  }
1105
1191
  periodStartSQL() {
1106
- let sql4 = "NOW()";
1192
+ let sql5 = "NOW()";
1107
1193
  if (this.offset !== 0) {
1108
- sql4 = `${sql4} + INTERVAL '${this.offset} ${this.period}'`;
1194
+ sql5 = `${sql5} + INTERVAL '${this.offset} ${this.period}'`;
1109
1195
  }
1110
1196
  if (this.complete) {
1111
- sql4 = `DATE_TRUNC('${this.period}', ${sql4})`;
1197
+ sql5 = `DATE_TRUNC('${this.period}', ${sql5})`;
1112
1198
  } else {
1113
- sql4 = `(${sql4})`;
1199
+ sql5 = `(${sql5})`;
1114
1200
  }
1115
- return sql4;
1201
+ return sql5;
1116
1202
  }
1117
1203
  periodEndSQL() {
1118
- let sql4 = this.periodStartSQL();
1204
+ let sql5 = this.periodStartSQL();
1119
1205
  if (this.value != 0) {
1120
- sql4 = `(${sql4} + INTERVAL '${this.value} ${this.period}')`;
1206
+ sql5 = `(${sql5} + INTERVAL '${this.value} ${this.period}')`;
1121
1207
  }
1122
- return sql4;
1208
+ return sql5;
1123
1209
  }
1124
1210
  };
1125
1211
 
@@ -1833,6 +1919,7 @@ var QueryBuilder = class _QueryBuilder {
1833
1919
  };
1834
1920
 
1835
1921
  // src/ModelAPI.js
1922
+ init_File();
1836
1923
  var ModelAPI = class {
1837
1924
  static {
1838
1925
  __name(this, "ModelAPI");
@@ -1842,9 +1929,10 @@ var ModelAPI = class {
1842
1929
  * @param {Function} _ Used to be a function that returns the default values for a row in this table. No longer used.
1843
1930
  * @param {TableConfigMap} tableConfigMap
1844
1931
  */
1845
- constructor(tableName, _, tableConfigMap = {}) {
1932
+ constructor(tableName, _, tableConfigMap = {}, fileFieldsMap = {}) {
1846
1933
  this._tableName = tableName;
1847
1934
  this._tableConfigMap = tableConfigMap;
1935
+ this._fileFields = fileFieldsMap[tableName] || {};
1848
1936
  this._modelName = upperCamelCase(this._tableName);
1849
1937
  }
1850
1938
  async create(values) {
@@ -1914,6 +2002,10 @@ var ModelAPI = class {
1914
2002
  const name = spanNameForModelAPI(this._modelName, "update");
1915
2003
  const db = useDatabase();
1916
2004
  return withSpan(name, async (span) => {
2005
+ const fileColumns = Object.keys(values || {}).filter(
2006
+ (k) => k in this._fileFields
2007
+ );
2008
+ const existingFileRows = fileColumns.length > 0 ? await this._selectExistingFileValues(where, fileColumns) : [];
1917
2009
  let builder = db.updateTable(this._tableName).returningAll();
1918
2010
  const keys = values ? Object.keys(values) : [];
1919
2011
  const row = {};
@@ -1952,7 +2044,9 @@ var ModelAPI = class {
1952
2044
  span.setAttribute("sql", builder.compile().sql);
1953
2045
  try {
1954
2046
  const row2 = await builder.executeTakeFirstOrThrow();
1955
- return transformRichDataTypes(camelCaseObject(row2));
2047
+ const result = transformRichDataTypes(camelCaseObject(row2));
2048
+ this._deferReplacedFiles(existingFileRows, fileColumns, result);
2049
+ return result;
1956
2050
  } catch (e) {
1957
2051
  throw new DatabaseError(e);
1958
2052
  }
@@ -1962,12 +2056,15 @@ var ModelAPI = class {
1962
2056
  const name = spanNameForModelAPI(this._modelName, "delete");
1963
2057
  const db = useDatabase();
1964
2058
  return withSpan(name, async (span) => {
2059
+ const fileColumns = Object.keys(this._fileFields);
2060
+ const existingFileRows = fileColumns.length > 0 ? await this._selectExistingFileValues(where, fileColumns) : [];
1965
2061
  let builder = db.deleteFrom(this._tableName).returning(["id"]);
1966
2062
  const context7 = new QueryContext([this._tableName], this._tableConfigMap);
1967
2063
  builder = applyWhereConditions(context7, builder, where);
1968
2064
  span.setAttribute("sql", builder.compile().sql);
1969
2065
  try {
1970
2066
  const row = await builder.executeTakeFirstOrThrow();
2067
+ this._deferReplacedFiles(existingFileRows, fileColumns, null);
1971
2068
  return row.id;
1972
2069
  } catch (e) {
1973
2070
  throw new DatabaseError(e);
@@ -1982,6 +2079,30 @@ var ModelAPI = class {
1982
2079
  builder = applyWhereConditions(context7, builder, where);
1983
2080
  return new QueryBuilder(this._tableName, context7, builder);
1984
2081
  }
2082
+ // Reads the current file-column values for rows matched by `where`.
2083
+ async _selectExistingFileValues(where, fileColumns) {
2084
+ if (fileColumns.length === 0) {
2085
+ return [];
2086
+ }
2087
+ const db = useDatabase();
2088
+ let builder = db.selectFrom(this._tableName).selectAll(this._tableName);
2089
+ const context7 = new QueryContext([this._tableName], this._tableConfigMap);
2090
+ builder = applyWhereConditions(context7, builder, where);
2091
+ const rows = await builder.execute();
2092
+ return rows.map((x) => transformRichDataTypes(camelCaseObject(x)));
2093
+ }
2094
+ // Defers deletion of every old file key in existingRows that is not still
2095
+ // referenced by newRow.
2096
+ _deferReplacedFiles(existingRows, fileColumns, newRow) {
2097
+ const retained = new Set(
2098
+ collectFileKeys(newRow ? [newRow] : [], fileColumns)
2099
+ );
2100
+ for (const key of collectFileKeys(existingRows, fileColumns)) {
2101
+ if (!retained.has(key)) {
2102
+ deferFileDeletion(key);
2103
+ }
2104
+ }
2105
+ }
1985
2106
  };
1986
2107
  async function create(conn, tableName, tableConfigs, values) {
1987
2108
  try {
@@ -2091,6 +2212,23 @@ async function create(conn, tableName, tableConfigs, values) {
2091
2212
  }
2092
2213
  }
2093
2214
  __name(create, "create");
2215
+ function collectFileKeys(rows, fileColumns) {
2216
+ const keys = [];
2217
+ for (const row of rows || []) {
2218
+ if (!row) continue;
2219
+ for (const col of fileColumns) {
2220
+ const v = row[col];
2221
+ if (!v) continue;
2222
+ if (Array.isArray(v)) {
2223
+ for (const f of v) if (f?.key) keys.push(f.key);
2224
+ } else if (v.key) {
2225
+ keys.push(v.key);
2226
+ }
2227
+ }
2228
+ }
2229
+ return keys;
2230
+ }
2231
+ __name(collectFileKeys, "collectFileKeys");
2094
2232
 
2095
2233
  // src/TaskAPI.js
2096
2234
  import jwt from "jsonwebtoken";
@@ -2636,6 +2774,51 @@ var FlowsAPI = class _FlowsAPI {
2636
2774
  );
2637
2775
  });
2638
2776
  }
2777
+ /**
2778
+ * Mints a signed link that lets an external, unauthenticated actor execute this flow. The flow
2779
+ * must be declared with @externalAccess. Each time the link is opened a fresh flow run is created;
2780
+ * a reusable link can be opened many times, otherwise it permits exactly one run.
2781
+ * @param {Object} [options] Signed link options
2782
+ * @param {Object} [options.inputs] Default inputs applied to every run created from the link
2783
+ * @param {Date} [options.expiresAt] When the link stops being valid
2784
+ * @param {boolean} [options.reusable] Whether the link can be opened more than once
2785
+ * @returns {Promise<{url: string, expiresAt: Date|null, flow: {name: string, runId: string|null}}>} The signed link
2786
+ */
2787
+ async signedLink(options = {}) {
2788
+ const name = spanNameForModelAPI(this._flowName, "signedLink");
2789
+ return withSpan(name, async () => {
2790
+ const apiUrl = getApiUrl2();
2791
+ const url = `${apiUrl}/flows/json/${encodeURIComponent(
2792
+ this._flowName
2793
+ )}/share`;
2794
+ const body = {};
2795
+ if (options.inputs !== void 0) body.inputs = options.inputs;
2796
+ if (options.reusable !== void 0) body.reusable = options.reusable;
2797
+ if (options.expiresAt !== void 0 && options.expiresAt !== null) {
2798
+ body.expiresAt = options.expiresAt instanceof Date ? options.expiresAt.toISOString() : options.expiresAt;
2799
+ }
2800
+ const response = await fetch(url, {
2801
+ method: "POST",
2802
+ headers: buildHeaders2(this._identity, this._authToken),
2803
+ body: JSON.stringify(body)
2804
+ });
2805
+ if (!response.ok) {
2806
+ const errorBody = await response.json().catch(() => ({}));
2807
+ throw new Error(
2808
+ `Failed to create signed link: ${response.status} ${response.statusText} - ${errorBody.message || JSON.stringify(errorBody)}`
2809
+ );
2810
+ }
2811
+ const result = await response.json();
2812
+ return {
2813
+ url: result.url,
2814
+ expiresAt: result.expiresAt ? new Date(result.expiresAt) : null,
2815
+ flow: {
2816
+ name: result.flow?.name ?? this._flowName,
2817
+ runId: result.flow?.runId ?? null
2818
+ }
2819
+ };
2820
+ });
2821
+ }
2639
2822
  /**
2640
2823
  * Gets a flow run by ID.
2641
2824
  * @param {string} runId The flow run ID
@@ -2717,6 +2900,61 @@ var FlowsAPI = class _FlowsAPI {
2717
2900
  }
2718
2901
  };
2719
2902
 
2903
+ // src/integrationServer.js
2904
+ import jwt3 from "jsonwebtoken";
2905
+ function buildHeaders3(identity) {
2906
+ const headers = { "Content-Type": "application/json" };
2907
+ const base64pk = process.env.KEEL_PRIVATE_KEY;
2908
+ if (!base64pk) {
2909
+ throw new Error(
2910
+ "KEEL_PRIVATE_KEY is not set; cannot sign the integration proxy token"
2911
+ );
2912
+ }
2913
+ const privateKey = Buffer.from(base64pk, "base64").toString("utf8");
2914
+ const subject = identity && identity.id ? identity.id : "integration-proxy";
2915
+ headers["Authorization"] = "Bearer " + jwt3.sign({}, privateKey, {
2916
+ algorithm: "RS256",
2917
+ expiresIn: 60 * 60 * 24,
2918
+ subject,
2919
+ // Scope the token to the integration proxy so it can't be mistaken for an ordinary user
2920
+ // access token; the runtime proxy verifies this audience before injecting credentials.
2921
+ audience: "integration-proxy",
2922
+ issuer: "https://keel.so"
2923
+ });
2924
+ return headers;
2925
+ }
2926
+ __name(buildHeaders3, "buildHeaders");
2927
+ function getApiUrl3() {
2928
+ const apiUrl = process.env.KEEL_API_URL;
2929
+ if (!apiUrl) {
2930
+ throw new Error("KEEL_API_URL environment variable is not set");
2931
+ }
2932
+ return apiUrl;
2933
+ }
2934
+ __name(getApiUrl3, "getApiUrl");
2935
+ function createIntegrationServer(name, identity) {
2936
+ return {
2937
+ do: /* @__PURE__ */ __name(async (request) => {
2938
+ const url = `${getApiUrl3()}/integrations/${encodeURIComponent(
2939
+ name
2940
+ )}/proxy`;
2941
+ const response = await fetch(url, {
2942
+ method: "POST",
2943
+ headers: buildHeaders3(identity ?? null),
2944
+ body: JSON.stringify(request ?? {})
2945
+ });
2946
+ if (!response.ok) {
2947
+ const text = await response.text();
2948
+ throw new Error(
2949
+ `integration "${name}" proxy request failed (${response.status}): ${text}`
2950
+ );
2951
+ }
2952
+ return await response.json();
2953
+ }, "do")
2954
+ };
2955
+ }
2956
+ __name(createIntegrationServer, "createIntegrationServer");
2957
+
2720
2958
  // src/RequestHeaders.ts
2721
2959
  var RequestHeaders = class {
2722
2960
  /**
@@ -3917,6 +4155,7 @@ var datePickerInput = /* @__PURE__ */ __name((name, options) => {
3917
4155
  }, "datePickerInput");
3918
4156
 
3919
4157
  // src/flows/ui/elements/input/file.ts
4158
+ init_File();
3920
4159
  var fileInput = /* @__PURE__ */ __name((name, options) => {
3921
4160
  return {
3922
4161
  __type: "input",
@@ -3942,6 +4181,7 @@ var fileInput = /* @__PURE__ */ __name((name, options) => {
3942
4181
  }, "fileInput");
3943
4182
 
3944
4183
  // src/flows/ui/elements/input/imageCapture.ts
4184
+ init_File();
3945
4185
  var isMultiOptions = /* @__PURE__ */ __name((opts) => opts && opts.mode === "multi", "isMultiOptions");
3946
4186
  function validateEntry(entry, requireCaption, context7) {
3947
4187
  if (!entry?.file?.key) {
@@ -4174,6 +4414,233 @@ var file = /* @__PURE__ */ __name(async (options) => {
4174
4414
  }, "file");
4175
4415
 
4176
4416
  // src/flows/index.ts
4417
+ import { sql as sql4 } from "kysely";
4418
+
4419
+ // src/agents/engine.ts
4420
+ var ENGINE = Symbol.for("keel.agents.engine");
4421
+ var ScriptedEngine = class {
4422
+ constructor(script) {
4423
+ this.script = script;
4424
+ this._requests = [];
4425
+ this.index = 0;
4426
+ }
4427
+ static {
4428
+ __name(this, "ScriptedEngine");
4429
+ }
4430
+ get requests() {
4431
+ return this._requests;
4432
+ }
4433
+ async turn(req) {
4434
+ this._requests.push(req);
4435
+ if (this.index >= this.script.length) {
4436
+ throw new Error(
4437
+ `ScriptedEngine: no scripted result for turn ${this.index + 1}`
4438
+ );
4439
+ }
4440
+ const entry = this.script[this.index++];
4441
+ return typeof entry === "function" ? entry(req) : entry;
4442
+ }
4443
+ };
4444
+ var callCounter = 0;
4445
+ var scriptedTurn = {
4446
+ text(text) {
4447
+ return {
4448
+ text,
4449
+ toolCalls: [],
4450
+ usage: { inputTokens: 0, outputTokens: 0 },
4451
+ stopReason: "end_turn"
4452
+ };
4453
+ },
4454
+ /** Auto-generated ids increment per process — pass an explicit `id` when asserting on ids in tests. */
4455
+ toolCall(name, args, id) {
4456
+ return {
4457
+ text: "",
4458
+ toolCalls: [
4459
+ { id: id ?? `call_${name}_${++callCounter}`, name, arguments: args }
4460
+ ],
4461
+ usage: { inputTokens: 0, outputTokens: 0 },
4462
+ stopReason: "tool_calls"
4463
+ };
4464
+ }
4465
+ };
4466
+
4467
+ // src/agents/engineResolver.ts
4468
+ import { readFileSync as readFileSync2 } from "fs";
4469
+
4470
+ // src/agents/aiSdkEngine.ts
4471
+ import {
4472
+ generateText,
4473
+ jsonSchema,
4474
+ tool,
4475
+ stepCountIs
4476
+ } from "ai";
4477
+ import { createAnthropic } from "@ai-sdk/anthropic";
4478
+ var AiSdkEngine = class {
4479
+ constructor(opts) {
4480
+ this.opts = opts;
4481
+ }
4482
+ static {
4483
+ __name(this, "AiSdkEngine");
4484
+ }
4485
+ // Exposed as a method (not arrow function) so tests can monkey-patch at the
4486
+ // instance level: (engine as any).resolveModel = () => mockModel(...)
4487
+ resolveModel(model) {
4488
+ const slash = model.indexOf("/");
4489
+ const provider = slash === -1 ? "anthropic" : model.slice(0, slash);
4490
+ const id = slash === -1 ? model : model.slice(slash + 1);
4491
+ if (provider !== "anthropic") {
4492
+ throw new Error(
4493
+ `Unsupported provider "${provider}" in model "${model}" \u2014 only anthropic/* is supported in this milestone`
4494
+ );
4495
+ }
4496
+ const baseURL = this.opts.baseURL ?? "https://api.anthropic.com/v1";
4497
+ if (this.opts.authToken) {
4498
+ return createAnthropic({
4499
+ apiKey: "unused",
4500
+ baseURL,
4501
+ headers: {
4502
+ "x-api-key": "",
4503
+ authorization: `Bearer ${this.opts.authToken}`,
4504
+ "anthropic-beta": "oauth-2025-04-20"
4505
+ }
4506
+ })(id);
4507
+ }
4508
+ if (!this.opts.apiKey) {
4509
+ throw new Error("AiSdkEngine requires an apiKey or authToken");
4510
+ }
4511
+ return createAnthropic({
4512
+ apiKey: this.opts.apiKey,
4513
+ baseURL
4514
+ })(id);
4515
+ }
4516
+ async turn(req) {
4517
+ const model = this.resolveModel(req.model);
4518
+ const tools = {};
4519
+ for (const spec of req.tools ?? []) {
4520
+ tools[spec.name] = tool({
4521
+ description: spec.description,
4522
+ inputSchema: jsonSchema(
4523
+ spec.parameters
4524
+ )
4525
+ });
4526
+ }
4527
+ let toolChoice;
4528
+ if (req.toolChoice === "auto") {
4529
+ toolChoice = "auto";
4530
+ } else if (req.toolChoice && typeof req.toolChoice === "object") {
4531
+ toolChoice = { type: "tool", toolName: req.toolChoice.name };
4532
+ }
4533
+ const messages = req.messages.map((msg) => {
4534
+ if (msg.role === "user") {
4535
+ return { role: "user", content: msg.content };
4536
+ }
4537
+ if (msg.role === "assistant") {
4538
+ const content = [];
4539
+ if (msg.content) {
4540
+ content.push({ type: "text", text: msg.content });
4541
+ }
4542
+ for (const tc of msg.toolCalls ?? []) {
4543
+ content.push({
4544
+ type: "tool-call",
4545
+ toolCallId: tc.id,
4546
+ toolName: tc.name,
4547
+ input: tc.arguments
4548
+ });
4549
+ }
4550
+ return { role: "assistant", content };
4551
+ }
4552
+ return {
4553
+ role: "tool",
4554
+ content: [
4555
+ {
4556
+ type: "tool-result",
4557
+ toolCallId: msg.toolCallId,
4558
+ toolName: msg.toolName,
4559
+ output: { type: "text", value: msg.result }
4560
+ }
4561
+ ]
4562
+ };
4563
+ });
4564
+ let res;
4565
+ try {
4566
+ res = await generateText({
4567
+ model,
4568
+ system: req.system,
4569
+ messages,
4570
+ tools: Object.keys(tools).length > 0 ? tools : void 0,
4571
+ toolChoice,
4572
+ stopWhen: stepCountIs(1)
4573
+ });
4574
+ } catch (e) {
4575
+ throw new Error(describeAiError(e));
4576
+ }
4577
+ const toolCalls = res.toolCalls.map((tc) => ({
4578
+ id: tc.toolCallId,
4579
+ name: tc.toolName,
4580
+ arguments: tc.input
4581
+ }));
4582
+ return {
4583
+ text: res.text,
4584
+ toolCalls,
4585
+ usage: {
4586
+ inputTokens: res.usage.inputTokens ?? 0,
4587
+ outputTokens: res.usage.outputTokens ?? 0
4588
+ },
4589
+ stopReason: toolCalls.length > 0 ? "tool_calls" : "end_turn"
4590
+ };
4591
+ }
4592
+ };
4593
+ function describeAiError(e) {
4594
+ const errors = e?.errors;
4595
+ const last = errors && errors.length > 0 ? errors[errors.length - 1] : e;
4596
+ const err = last;
4597
+ let msg = "LLM request failed";
4598
+ if (err?.statusCode) msg += ` (HTTP ${err.statusCode})`;
4599
+ const detail = err?.responseBody?.slice(0, 300) || (err?.message && err.message !== "Error" ? err.message : void 0);
4600
+ if (detail) msg += `: ${detail}`;
4601
+ if (err?.url) msg += ` [${err.url}]`;
4602
+ return msg;
4603
+ }
4604
+ __name(describeAiError, "describeAiError");
4605
+
4606
+ // src/agents/engineResolver.ts
4607
+ var _fileScriptedEngine;
4608
+ function resolveEngine(injected, secrets) {
4609
+ if (injected) return injected;
4610
+ const scriptPath = process.env.FAKE_LLM_SCRIPT;
4611
+ if (scriptPath) {
4612
+ if (!_fileScriptedEngine || _fileScriptedEngine.path !== scriptPath) {
4613
+ const script = JSON.parse(readFileSync2(scriptPath, "utf8"));
4614
+ _fileScriptedEngine = {
4615
+ path: scriptPath,
4616
+ engine: new ScriptedEngine(script)
4617
+ };
4618
+ }
4619
+ return _fileScriptedEngine.engine;
4620
+ }
4621
+ const key = secrets?.["ANTHROPIC_API_KEY"];
4622
+ if (!key) {
4623
+ throw new Error(
4624
+ "No LLM engine available \u2014 set the ANTHROPIC_API_KEY secret, or inject an engine in tests"
4625
+ );
4626
+ }
4627
+ if (key.startsWith("sk-ant-oat")) {
4628
+ return new AiSdkEngine({ authToken: key });
4629
+ }
4630
+ return new AiSdkEngine({ apiKey: key });
4631
+ }
4632
+ __name(resolveEngine, "resolveEngine");
4633
+ function resolveEngineForContext(context7) {
4634
+ const get = context7?.[ENGINE];
4635
+ if (typeof get !== "function") {
4636
+ throw new Error("An LLM engine is only available within a flow context");
4637
+ }
4638
+ return get();
4639
+ }
4640
+ __name(resolveEngineForContext, "resolveEngineForContext");
4641
+
4642
+ // src/flows/index.ts
4643
+ import KSUID2 from "ksuid";
4177
4644
  var STEP_STATUS = /* @__PURE__ */ ((STEP_STATUS2) => {
4178
4645
  STEP_STATUS2["NEW"] = "NEW";
4179
4646
  STEP_STATUS2["RUNNING"] = "RUNNING";
@@ -4210,28 +4677,30 @@ var defaultOpts = {
4210
4677
  async function insertNewStep(db, runId, name, stage) {
4211
4678
  await db.transaction().execute(async (trx) => {
4212
4679
  await trx.selectFrom("keel.flow_run").select("id").where("id", "=", runId).forUpdate().executeTakeFirst();
4213
- const existing = await trx.selectFrom("keel.flow_step").select("id").where("run_id", "=", runId).where("name", "=", name).where("status", "=", "NEW" /* NEW */).executeTakeFirst();
4680
+ 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();
4214
4681
  if (existing) {
4215
4682
  return;
4216
4683
  }
4217
4684
  await trx.insertInto("keel.flow_step").values({
4685
+ id: KSUID2.randomSync().string,
4218
4686
  run_id: runId,
4219
4687
  name,
4220
4688
  stage,
4221
4689
  status: "NEW" /* NEW */,
4222
4690
  type: "FUNCTION" /* FUNCTION */
4223
- }).execute();
4691
+ }).returningAll().executeTakeFirst();
4224
4692
  });
4225
4693
  }
4226
4694
  __name(insertNewStep, "insertNewStep");
4227
4695
  function createFlowContext(runId, data, action, callback, element, spanId, ctx) {
4228
4696
  const usedNames = /* @__PURE__ */ new Set();
4229
- return {
4697
+ const context7 = {
4230
4698
  identity: ctx.identity,
4231
4699
  env: ctx.env,
4232
4700
  now: ctx.now,
4233
4701
  secrets: ctx.secrets,
4234
4702
  trace: ctx.trace,
4703
+ module: ctx.module,
4235
4704
  complete: /* @__PURE__ */ __name((options) => {
4236
4705
  return {
4237
4706
  __type: "ui.complete",
@@ -4251,6 +4720,7 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
4251
4720
  if (usedNames.has(name)) {
4252
4721
  span.setAttribute("step.status", "FAILED" /* FAILED */);
4253
4722
  await db.insertInto("keel.flow_step").values({
4723
+ id: KSUID2.randomSync().string,
4254
4724
  run_id: runId,
4255
4725
  name,
4256
4726
  stage: options.stage,
@@ -4263,7 +4733,7 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
4263
4733
  throw new Error(`Duplicate step name: ${name}`);
4264
4734
  }
4265
4735
  usedNames.add(name);
4266
- const past = await db.selectFrom("keel.flow_step").where("run_id", "=", runId).where("name", "=", name).selectAll().execute();
4736
+ const past = await db.selectFrom("keel.flow_step").where("run_id", "=", runId).where("name", "=", name).selectAll().select(sql4`value::text`.as("valueRaw")).execute();
4267
4737
  const newSteps = past.filter(
4268
4738
  (step) => step.status === "NEW" /* NEW */
4269
4739
  );
@@ -4290,7 +4760,13 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
4290
4760
  if (completedSteps.length === 1) {
4291
4761
  span.setAttribute(KEEL_INTERNAL_ATTR, KEEL_INTERNAL_CHILDREN);
4292
4762
  span.setAttribute("step.status", "COMPLETED" /* COMPLETED */);
4293
- return completedSteps[0].value;
4763
+ const raw = completedSteps[0].valueRaw;
4764
+ return raw == null ? void 0 : JSON.parse(raw);
4765
+ }
4766
+ if (runningSteps.length >= 1) {
4767
+ span.setAttribute(KEEL_INTERNAL_ATTR, KEEL_INTERNAL_CHILDREN);
4768
+ span.setAttribute("step.status", "RUNNING" /* RUNNING */);
4769
+ throw new StepCreatedDisrupt();
4294
4770
  }
4295
4771
  if (newSteps.length === 1) {
4296
4772
  let result = null;
@@ -4316,7 +4792,14 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
4316
4792
  endTime: /* @__PURE__ */ new Date(),
4317
4793
  error: e instanceof Error ? e.message : "An error occurred"
4318
4794
  }).where("id", "=", newSteps[0].id).returningAll().executeTakeFirst();
4319
- if (failedSteps.length >= options.retries || e instanceof NonRetriableError) {
4795
+ if (e instanceof NonRetriableError) {
4796
+ span.setAttribute("step.status", "FAILED" /* FAILED */);
4797
+ if (options.onFailure) {
4798
+ await options.onFailure();
4799
+ }
4800
+ throw e;
4801
+ }
4802
+ if (failedSteps.length >= options.retries) {
4320
4803
  span.setAttribute("step.status", "FAILED" /* FAILED */);
4321
4804
  if (options.onFailure) {
4322
4805
  await options.onFailure();
@@ -4340,11 +4823,6 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
4340
4823
  span.setAttribute("step.status", "COMPLETED" /* COMPLETED */);
4341
4824
  return result;
4342
4825
  }
4343
- if (runningSteps.length >= 1) {
4344
- span.setAttribute(KEEL_INTERNAL_ATTR, KEEL_INTERNAL_CHILDREN);
4345
- span.setAttribute("step.status", "RUNNING" /* RUNNING */);
4346
- throw new StepCreatedDisrupt();
4347
- }
4348
4826
  await insertNewStep(db, runId, name, options.stage);
4349
4827
  span.setAttribute(KEEL_INTERNAL_ATTR, KEEL_INTERNAL_CHILDREN);
4350
4828
  span.setAttribute("step.status", "NEW" /* NEW */);
@@ -4363,6 +4841,7 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
4363
4841
  if (usedNames.has(name)) {
4364
4842
  span.setAttribute("step.status", "FAILED" /* FAILED */);
4365
4843
  await db.insertInto("keel.flow_step").values({
4844
+ id: KSUID2.randomSync().string,
4366
4845
  run_id: runId,
4367
4846
  name,
4368
4847
  stage: options.stage,
@@ -4377,11 +4856,12 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
4377
4856
  usedNames.add(name);
4378
4857
  const { step, inserted } = await db.transaction().execute(async (trx) => {
4379
4858
  await trx.selectFrom("keel.flow_run").select("id").where("id", "=", runId).forUpdate().executeTakeFirst();
4380
- const existing = await trx.selectFrom("keel.flow_step").where("run_id", "=", runId).where("name", "=", name).selectAll().executeTakeFirst();
4859
+ const existing = await trx.selectFrom("keel.flow_step").where("run_id", "=", runId).where("name", "=", name).selectAll().select(sql4`value::text`.as("valueRaw")).executeTakeFirst();
4381
4860
  if (existing) {
4382
4861
  return { step: existing, inserted: false };
4383
4862
  }
4384
4863
  const created = await trx.insertInto("keel.flow_step").values({
4864
+ id: KSUID2.randomSync().string,
4385
4865
  run_id: runId,
4386
4866
  name,
4387
4867
  stage: options.stage,
@@ -4394,9 +4874,11 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
4394
4874
  if (step && step.status === "COMPLETED" /* COMPLETED */) {
4395
4875
  span.setAttribute(KEEL_INTERNAL_ATTR, KEEL_INTERNAL_CHILDREN);
4396
4876
  span.setAttribute("step.status", "COMPLETED" /* COMPLETED */);
4877
+ const rawValue = step.valueRaw;
4878
+ const storedData = rawValue == null ? null : JSON.parse(rawValue);
4397
4879
  const parsedData2 = await applyElementGetData(
4398
4880
  options.content,
4399
- transformRichDataTypes(step.value)
4881
+ transformRichDataTypes(storedData)
4400
4882
  );
4401
4883
  if (step.action) {
4402
4884
  return { data: parsedData2, action: step.action };
@@ -4512,6 +4994,8 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
4512
4994
  }
4513
4995
  }
4514
4996
  };
4997
+ context7[ENGINE] = () => resolveEngine(ctx.engine, ctx.secrets);
4998
+ return context7;
4515
4999
  }
4516
5000
  __name(createFlowContext, "createFlowContext");
4517
5001
  function wait(milliseconds) {
@@ -4718,19 +5202,558 @@ async function handleFlow(request, config) {
4718
5202
  __name(handleFlow, "handleFlow");
4719
5203
 
4720
5204
  // src/index.ts
4721
- import KSUID2 from "ksuid";
5205
+ import KSUID4 from "ksuid";
5206
+ init_File();
5207
+
5208
+ // src/notifications/email-template.tsx
5209
+ import {
5210
+ Html,
5211
+ Head,
5212
+ Body,
5213
+ Container,
5214
+ Heading,
5215
+ Text,
5216
+ Button,
5217
+ Section
5218
+ } from "@react-email/components";
5219
+ import { render } from "@react-email/render";
5220
+ import { jsx, jsxs } from "react/jsx-runtime";
5221
+ function renderBody(text) {
5222
+ return text.split("\n").flatMap((line, i) => i === 0 ? [line] : [/* @__PURE__ */ jsx("br", {}, `br-${i}`), line]);
5223
+ }
5224
+ __name(renderBody, "renderBody");
5225
+ var StockEmailTemplate = /* @__PURE__ */ __name(({ content }) => /* @__PURE__ */ jsxs(Html, { children: [
5226
+ /* @__PURE__ */ jsx(Head, {}),
5227
+ /* @__PURE__ */ jsx(
5228
+ Body,
5229
+ {
5230
+ style: {
5231
+ backgroundColor: "#f6f6f6",
5232
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
5233
+ },
5234
+ children: /* @__PURE__ */ jsxs(
5235
+ Container,
5236
+ {
5237
+ style: {
5238
+ backgroundColor: "#ffffff",
5239
+ margin: "40px auto",
5240
+ padding: "40px",
5241
+ maxWidth: "560px",
5242
+ borderRadius: "8px"
5243
+ },
5244
+ children: [
5245
+ content.title && /* @__PURE__ */ jsx(
5246
+ Heading,
5247
+ {
5248
+ style: {
5249
+ fontSize: "22px",
5250
+ fontWeight: 600,
5251
+ color: "#111827",
5252
+ margin: "0 0 16px"
5253
+ },
5254
+ children: content.title
5255
+ }
5256
+ ),
5257
+ /* @__PURE__ */ jsx(
5258
+ Text,
5259
+ {
5260
+ style: {
5261
+ fontSize: "15px",
5262
+ lineHeight: "1.6",
5263
+ color: "#374151",
5264
+ margin: "0 0 24px"
5265
+ },
5266
+ children: renderBody(content.body)
5267
+ }
5268
+ ),
5269
+ (content.actions ?? []).length > 0 && /* @__PURE__ */ jsx(Section, { style: { margin: "0 0 12px" }, children: content.actions.map((action, i) => /* @__PURE__ */ jsx(
5270
+ Button,
5271
+ {
5272
+ href: action.url,
5273
+ style: {
5274
+ backgroundColor: "#111827",
5275
+ color: "#ffffff",
5276
+ padding: "12px 24px",
5277
+ borderRadius: "6px",
5278
+ fontSize: "14px",
5279
+ fontWeight: 600,
5280
+ textDecoration: "none",
5281
+ display: "inline-block",
5282
+ // Lay buttons out side by side; space between adjacent buttons.
5283
+ marginRight: i < content.actions.length - 1 ? "12px" : "0"
5284
+ },
5285
+ children: action.label
5286
+ },
5287
+ i
5288
+ )) })
5289
+ ]
5290
+ }
5291
+ )
5292
+ }
5293
+ )
5294
+ ] }), "StockEmailTemplate");
5295
+ async function renderEmailHtml(content) {
5296
+ return render(/* @__PURE__ */ jsx(StockEmailTemplate, { content }), { pretty: false });
5297
+ }
5298
+ __name(renderEmailHtml, "renderEmailHtml");
5299
+
5300
+ // src/notifications/notify.ts
5301
+ import KSUID3 from "ksuid";
5302
+ function getApiUrl4() {
5303
+ const apiUrl = process.env.KEEL_API_URL;
5304
+ if (!apiUrl) {
5305
+ throw new Error("KEEL_API_URL environment variable is not set");
5306
+ }
5307
+ return apiUrl;
5308
+ }
5309
+ __name(getApiUrl4, "getApiUrl");
5310
+ function toArray(v) {
5311
+ if (v === void 0) return [];
5312
+ return Array.isArray(v) ? v : [v];
5313
+ }
5314
+ __name(toArray, "toArray");
5315
+ function normaliseGroup(group) {
5316
+ if (!group) return [];
5317
+ const refs = [];
5318
+ for (const email of toArray(group.emails)) {
5319
+ refs.push({ kind: "email", value: email });
5320
+ }
5321
+ for (const user of toArray(group.users)) {
5322
+ refs.push({
5323
+ kind: "user",
5324
+ value: typeof user === "string" ? user : user.id
5325
+ });
5326
+ }
5327
+ for (const identity of toArray(group.identities)) {
5328
+ refs.push({
5329
+ kind: "identity",
5330
+ value: typeof identity === "string" ? identity : identity.id
5331
+ });
5332
+ }
5333
+ for (const team of toArray(group.teams)) {
5334
+ refs.push({ kind: "team", value: team });
5335
+ }
5336
+ return refs;
5337
+ }
5338
+ __name(normaliseGroup, "normaliseGroup");
5339
+ async function notifyEmail(input) {
5340
+ return withSpan("notify.email", async () => {
5341
+ const id = KSUID3.randomSync().string;
5342
+ let rendered;
5343
+ let content;
5344
+ if (typeof input.content === "string") {
5345
+ rendered = input.content;
5346
+ content = void 0;
5347
+ } else {
5348
+ rendered = await renderEmailHtml(input.content);
5349
+ content = input.content;
5350
+ }
5351
+ const body = {
5352
+ id,
5353
+ subject: input.subject,
5354
+ rendered,
5355
+ content,
5356
+ recipients: {
5357
+ to: normaliseGroup(input.recipients.to),
5358
+ cc: normaliseGroup(input.recipients.cc),
5359
+ bcc: normaliseGroup(input.recipients.bcc)
5360
+ }
5361
+ };
5362
+ const response = await fetch(`${getApiUrl4()}/notifications/json/email`, {
5363
+ method: "POST",
5364
+ headers: { "Content-Type": "application/json" },
5365
+ body: JSON.stringify(body)
5366
+ });
5367
+ if (!response.ok) {
5368
+ const errorBody = await response.json().catch(() => ({}));
5369
+ throw new Error(
5370
+ `Failed to send notification: ${response.status} ${response.statusText} - ${errorBody.message || JSON.stringify(errorBody)}`
5371
+ );
5372
+ }
5373
+ await response.body?.cancel();
5374
+ return id;
5375
+ });
5376
+ }
5377
+ __name(notifyEmail, "notifyEmail");
5378
+ function createNotifier() {
5379
+ return { email: notifyEmail };
5380
+ }
5381
+ __name(createNotifier, "createNotifier");
5382
+
5383
+ // src/experimental.ts
5384
+ var experimental_exports = {};
5385
+ __export(experimental_exports, {
5386
+ AiSdkEngine: () => AiSdkEngine,
5387
+ LlmFlowStep: () => LlmFlowStep,
5388
+ ScriptedEngine: () => ScriptedEngine,
5389
+ defineAgent: () => defineAgent,
5390
+ defineTool: () => defineTool,
5391
+ scriptedTurn: () => scriptedTurn
5392
+ });
5393
+
5394
+ // src/agents/schema.ts
5395
+ function canonicalStringify(value) {
5396
+ if (value === null || typeof value !== "object") {
5397
+ return JSON.stringify(value);
5398
+ }
5399
+ if (Array.isArray(value)) {
5400
+ return "[" + value.map((v) => v === void 0 ? "null" : canonicalStringify(v)).join(",") + "]";
5401
+ }
5402
+ const sorted = Object.keys(value).sort().filter((k) => value[k] !== void 0).map((k) => JSON.stringify(k) + ":" + canonicalStringify(value[k])).join(",");
5403
+ return "{" + sorted + "}";
5404
+ }
5405
+ __name(canonicalStringify, "canonicalStringify");
5406
+ function canonicalize(value) {
5407
+ if (value === void 0) return value;
5408
+ return JSON.parse(canonicalStringify(value));
5409
+ }
5410
+ __name(canonicalize, "canonicalize");
5411
+ async function validateSchema(schema, value) {
5412
+ let result = schema["~standard"].validate(value);
5413
+ if (result instanceof Promise) result = await result;
5414
+ if (result.issues) {
5415
+ const issues = result.issues.map((i) => {
5416
+ const path = (i.path ?? []).map(
5417
+ (p) => typeof p === "object" && p !== null && "key" in p ? String(p.key) : String(p)
5418
+ ).join(".");
5419
+ return path ? `${path}: ${i.message}` : i.message;
5420
+ }).join("; ");
5421
+ return { ok: false, issues };
5422
+ }
5423
+ return { ok: true, value: result.value };
5424
+ }
5425
+ __name(validateSchema, "validateSchema");
5426
+ async function toJsonSchema(schema) {
5427
+ const vendor = schema["~standard"].vendor;
5428
+ if (vendor === "zod") {
5429
+ const zod = await import("zod");
5430
+ const toJSONSchema = zod.toJSONSchema ?? zod.z?.toJSONSchema;
5431
+ if (typeof toJSONSchema !== "function") {
5432
+ throw new Error(
5433
+ "Deriving a JSON schema from a zod schema requires zod v4"
5434
+ );
5435
+ }
5436
+ return toJSONSchema(schema);
5437
+ }
5438
+ throw new Error(
5439
+ `Unsupported schema vendor "${vendor}" \u2014 agent result/parameter schemas currently support Zod v4`
5440
+ );
5441
+ }
5442
+ __name(toJsonSchema, "toJsonSchema");
5443
+
5444
+ // src/agents/defineAgent.ts
5445
+ var AGENT_NAME_RE = /^[A-Za-z][A-Za-z0-9]*$/;
5446
+ var runCounters = /* @__PURE__ */ new WeakMap();
5447
+ function nextRunNumber(ctx, agentName) {
5448
+ let byName = runCounters.get(ctx);
5449
+ if (!byName) {
5450
+ byName = /* @__PURE__ */ new Map();
5451
+ runCounters.set(ctx, byName);
5452
+ }
5453
+ const n = (byName.get(agentName) ?? 0) + 1;
5454
+ byName.set(agentName, n);
5455
+ return n;
5456
+ }
5457
+ __name(nextRunNumber, "nextRunNumber");
5458
+ async function runAgent(config, ctx, inputs) {
5459
+ const engine = resolveEngineForContext(ctx);
5460
+ const runNumber = nextRunNumber(ctx, config.name);
5461
+ const prefix = `${config.name}#${runNumber}`;
5462
+ const promptContext = await config.beforeRun?.(ctx, { inputs }) ?? {};
5463
+ let system = config.instructions;
5464
+ if (config.result) {
5465
+ system += "\n\nWhen the task is complete you MUST call complete_task with the final result.";
5466
+ }
5467
+ const userTools = config.tools ?? [];
5468
+ const toolSpecs = await Promise.all(
5469
+ userTools.map(async (t) => ({
5470
+ name: t.name,
5471
+ description: t.usage ? `${t.description}
5472
+ ${t.usage}` : t.description,
5473
+ parameters: await toJsonSchema(t.parameters)
5474
+ }))
5475
+ );
5476
+ if (config.result) {
5477
+ toolSpecs.push({
5478
+ name: "complete_task",
5479
+ description: "Call this when the task is complete, with the final result.",
5480
+ parameters: await toJsonSchema(config.result)
5481
+ });
5482
+ }
5483
+ const toolMap = new Map(
5484
+ userTools.map((t) => [t.name, t])
5485
+ );
5486
+ const messages = [
5487
+ {
5488
+ role: "user",
5489
+ content: config.prompt({ inputs, context: promptContext })
5490
+ }
5491
+ ];
5492
+ const maxTurns = config.maxTurns ?? 10;
5493
+ const maxCompletionNudges = 2;
5494
+ let nudges = 0;
5495
+ for (let turn = 1; turn <= maxTurns; turn++) {
5496
+ const res = canonicalize(
5497
+ await ctx.step(
5498
+ `${prefix}/turn-${turn}/llm`,
5499
+ { retries: 2, timeout: 3e5 },
5500
+ () => engine.turn({
5501
+ model: config.model,
5502
+ system,
5503
+ messages,
5504
+ ...toolSpecs.length ? { tools: toolSpecs } : {}
5505
+ })
5506
+ )
5507
+ );
5508
+ messages.push({
5509
+ role: "assistant",
5510
+ content: res.text,
5511
+ toolCalls: res.toolCalls
5512
+ });
5513
+ const toolCalls = res.toolCalls ?? [];
5514
+ const completeCall = toolCalls.find((c) => c.name === "complete_task");
5515
+ const otherCalls = toolCalls.filter((c) => c.name !== "complete_task");
5516
+ if (completeCall && config.result && otherCalls.length === 0) {
5517
+ const v = await validateSchema(config.result, completeCall.arguments);
5518
+ if (v.ok) {
5519
+ await config.afterRun?.(ctx, v.value);
5520
+ return v.value;
5521
+ }
5522
+ messages.push({
5523
+ role: "tool",
5524
+ toolCallId: completeCall.id,
5525
+ toolName: "complete_task",
5526
+ result: `Validation failed: ${v.issues}. Call complete_task again with corrected arguments.`
5527
+ });
5528
+ continue;
5529
+ }
5530
+ if (res.stopReason === "end_turn") {
5531
+ if (!config.result) {
5532
+ await config.afterRun?.(ctx, res.text);
5533
+ return res.text;
5534
+ }
5535
+ if (++nudges > maxCompletionNudges) {
5536
+ throw new Error(
5537
+ `Agent "${config.name}" finished without calling complete_task after ${maxCompletionNudges} reminders`
5538
+ );
5539
+ }
5540
+ messages.push({
5541
+ role: "user",
5542
+ content: "You must call complete_task with the final result."
5543
+ });
5544
+ continue;
5545
+ }
5546
+ if (completeCall) {
5547
+ messages.push({
5548
+ role: "tool",
5549
+ toolCallId: completeCall.id,
5550
+ toolName: "complete_task",
5551
+ 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(", ")}`
5552
+ });
5553
+ }
5554
+ for (let i = 0; i < otherCalls.length; i++) {
5555
+ const call = otherCalls[i];
5556
+ const tool2 = toolMap.get(call.name);
5557
+ if (!tool2) {
5558
+ const available = userTools.map((t) => t.name).join(", ");
5559
+ messages.push({
5560
+ role: "tool",
5561
+ toolCallId: call.id,
5562
+ toolName: call.name,
5563
+ result: `Unknown tool "${call.name}" \u2014 available tools: ${available}`
5564
+ });
5565
+ continue;
5566
+ }
5567
+ const approved = await checkApproval(
5568
+ ctx,
5569
+ prefix,
5570
+ turn,
5571
+ i + 1,
5572
+ tool2,
5573
+ call.arguments
5574
+ );
5575
+ if (!approved) {
5576
+ messages.push({
5577
+ role: "tool",
5578
+ toolCallId: call.id,
5579
+ toolName: call.name,
5580
+ result: `The user rejected this tool call. Do not retry it; choose another course of action.`
5581
+ });
5582
+ continue;
5583
+ }
5584
+ const stepResult = canonicalize(
5585
+ await ctx.step(
5586
+ `${prefix}/turn-${turn}/tool-${i + 1}:${call.name}`,
5587
+ { retries: 2, timeout: 6e4 },
5588
+ async () => {
5589
+ const v = await validateSchema(tool2.parameters, call.arguments);
5590
+ if (!v.ok) {
5591
+ return { ok: false, error: `Invalid arguments: ${v.issues}` };
5592
+ }
5593
+ return { ok: true, value: await tool2.execute(v.value) ?? null };
5594
+ }
5595
+ )
5596
+ );
5597
+ const resultText = stepResult.ok ? canonicalStringify(stepResult.value) : stepResult.error;
5598
+ messages.push({
5599
+ role: "tool",
5600
+ toolCallId: call.id,
5601
+ toolName: call.name,
5602
+ result: resultText
5603
+ });
5604
+ }
5605
+ }
5606
+ throw new Error(`Agent "${config.name}" exceeded maxTurns (${maxTurns})`);
5607
+ }
5608
+ __name(runAgent, "runAgent");
5609
+ async function checkApproval(ctx, prefix, turn, index, tool2, args) {
5610
+ const needsApproval = tool2.approval === true || typeof tool2.approval === "function" && tool2.approval(args);
5611
+ if (!needsApproval) return true;
5612
+ const result = await ctx.ui.page(
5613
+ `${prefix}/turn-${turn}/approve-${index}:${tool2.name}`,
5614
+ {
5615
+ title: `Approve tool call: ${tool2.name}`,
5616
+ content: [
5617
+ ctx.ui.display.markdown({
5618
+ content: `The agent wants to call **${tool2.name}** with:
5619
+
5620
+ \`\`\`json
5621
+ ${canonicalStringify(args)}
5622
+ \`\`\``
5623
+ })
5624
+ ],
5625
+ actions: [
5626
+ { label: "Approve", value: "approve" },
5627
+ { label: "Reject", value: "reject" }
5628
+ ]
5629
+ }
5630
+ );
5631
+ return result.action === "approve";
5632
+ }
5633
+ __name(checkApproval, "checkApproval");
5634
+ function defineAgent(config) {
5635
+ if (!AGENT_NAME_RE.test(config.name)) {
5636
+ throw new Error(
5637
+ `Invalid agent name "${config.name}" \u2014 must be alphanumeric PascalCase`
5638
+ );
5639
+ }
5640
+ const names = /* @__PURE__ */ new Set();
5641
+ for (const tool2 of config.tools ?? []) {
5642
+ if (names.has(tool2.name)) {
5643
+ throw new Error(
5644
+ `Duplicate tool "${tool2.name}" on agent "${config.name}"`
5645
+ );
5646
+ }
5647
+ names.add(tool2.name);
5648
+ }
5649
+ return Object.freeze({
5650
+ name: config.name,
5651
+ config,
5652
+ run: /* @__PURE__ */ __name((ctx, inputs) => runAgent(config, ctx, inputs), "run")
5653
+ });
5654
+ }
5655
+ __name(defineAgent, "defineAgent");
5656
+
5657
+ // src/agents/defineTool.ts
5658
+ var NAME_RE = /^[a-z][a-z0-9_]*$/;
5659
+ var RESERVED = /* @__PURE__ */ new Set(["complete_task"]);
5660
+ function defineTool(config) {
5661
+ if (!NAME_RE.test(config.name)) {
5662
+ throw new Error(
5663
+ `Invalid tool name "${config.name}" \u2014 must be snake_case ([a-z][a-z0-9_]*)`
5664
+ );
5665
+ }
5666
+ if (RESERVED.has(config.name)) {
5667
+ throw new Error(`Tool name "${config.name}" is reserved`);
5668
+ }
5669
+ return Object.freeze({ ...config });
5670
+ }
5671
+ __name(defineTool, "defineTool");
5672
+
5673
+ // src/agents/llm.ts
5674
+ async function LlmFlowStep(ctx, name, options) {
5675
+ const stepValue = await ctx.step(
5676
+ name,
5677
+ { retries: 2, timeout: options.timeout ?? 3e5 },
5678
+ async () => {
5679
+ const engine = resolveEngineForContext(ctx);
5680
+ if (!options.result) {
5681
+ const res = await engine.turn({
5682
+ model: options.model,
5683
+ system: options.system,
5684
+ messages: [{ role: "user", content: options.prompt }]
5685
+ });
5686
+ return res.text;
5687
+ }
5688
+ const parameters = await toJsonSchema(options.result);
5689
+ const messages = [
5690
+ { role: "user", content: options.prompt }
5691
+ ];
5692
+ for (let attempt = 0; attempt < 3; attempt++) {
5693
+ const res = await engine.turn({
5694
+ model: options.model,
5695
+ system: options.system,
5696
+ messages,
5697
+ tools: [
5698
+ {
5699
+ name: "complete_task",
5700
+ description: "Return the final result of the task.",
5701
+ parameters
5702
+ }
5703
+ ],
5704
+ toolChoice: { name: "complete_task" }
5705
+ });
5706
+ const call = res.toolCalls.find((c) => c.name === "complete_task");
5707
+ if (call) {
5708
+ const v = await validateSchema(options.result, call.arguments);
5709
+ if (v.ok) return v.value;
5710
+ messages.push(
5711
+ {
5712
+ role: "assistant",
5713
+ content: res.text,
5714
+ toolCalls: res.toolCalls
5715
+ },
5716
+ {
5717
+ role: "tool",
5718
+ toolCallId: call.id,
5719
+ toolName: "complete_task",
5720
+ result: `Validation failed: ${v.issues}. Call complete_task again with corrected arguments.`
5721
+ }
5722
+ );
5723
+ continue;
5724
+ }
5725
+ messages.push(
5726
+ { role: "assistant", content: res.text },
5727
+ {
5728
+ role: "user",
5729
+ content: "You must call complete_task with the final result."
5730
+ }
5731
+ );
5732
+ }
5733
+ throw new NonRetriableError(
5734
+ `LlmFlowStep("${name}"): result failed validation after 3 attempts`
5735
+ );
5736
+ }
5737
+ );
5738
+ return canonicalize(stepValue);
5739
+ }
5740
+ __name(LlmFlowStep, "LlmFlowStep");
5741
+
5742
+ // src/index.ts
5743
+ import { z } from "zod";
4722
5744
  var createTraceAPI2 = createTraceAPI;
4723
5745
  function ksuid() {
4724
- return KSUID2.randomSync().string;
5746
+ return KSUID4.randomSync().string;
4725
5747
  }
4726
5748
  __name(ksuid, "ksuid");
5749
+ var notify = createNotifier();
4727
5750
  export {
4728
5751
  Duration,
4729
5752
  ErrorPresets,
4730
5753
  File,
4731
5754
  FlowsAPI,
4732
5755
  InlineFile,
4733
- KSUID2 as KSUID,
5756
+ KSUID4 as KSUID,
4734
5757
  ModelAPI,
4735
5758
  NonRetriableError,
4736
5759
  PERMISSION_STATE,
@@ -4745,14 +5768,20 @@ export {
4745
5768
  TaskAPI,
4746
5769
  checkBuiltInPermissions,
4747
5770
  createFlowContext,
5771
+ createIntegrationServer,
5772
+ createNotifier,
4748
5773
  createTraceAPI2 as createTraceAPI,
5774
+ experimental_exports as experimental,
4749
5775
  handleFlow,
4750
5776
  handleJob,
4751
5777
  handleRequest,
4752
5778
  handleRoute,
4753
5779
  handleSubscriber,
5780
+ insertNewStep,
4754
5781
  ksuid,
5782
+ notify,
4755
5783
  tracing_exports as tracing,
4756
- useDatabase
5784
+ useDatabase,
5785
+ z
4757
5786
  };
4758
5787
  //# sourceMappingURL=index.js.map