braintrust 0.0.166 → 0.0.168

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.mjs CHANGED
@@ -274,7 +274,8 @@ import {
274
274
  promptDataSchema,
275
275
  promptSchema,
276
276
  toolsSchema,
277
- gitMetadataSettingsSchema
277
+ gitMetadataSettingsSchema,
278
+ BRAINTRUST_ATTACHMENT
278
279
  } from "@braintrust/core/typespecs";
279
280
 
280
281
  // src/util.ts
@@ -325,7 +326,7 @@ var LazyValue = class {
325
326
 
326
327
  // src/logger.ts
327
328
  import Mustache from "mustache";
328
- import { z as z2 } from "zod";
329
+ import { z as z2, ZodError } from "zod";
329
330
 
330
331
  // src/functions/stream.ts
331
332
  import {
@@ -615,6 +616,9 @@ var NoopSpan = class {
615
616
  async export() {
616
617
  return "";
617
618
  }
619
+ async permalink() {
620
+ return "";
621
+ }
618
622
  async flush() {
619
623
  }
620
624
  close(args) {
@@ -756,11 +760,11 @@ var BraintrustState = class _BraintrustState {
756
760
  state.loginReplaceApiConn(state.apiConn());
757
761
  return state;
758
762
  }
759
- setFetch(fetch) {
760
- this.loginParams.fetch = fetch;
761
- this.fetch = fetch;
762
- this._apiConn?.setFetch(fetch);
763
- this._appConn?.setFetch(fetch);
763
+ setFetch(fetch2) {
764
+ this.loginParams.fetch = fetch2;
765
+ this.fetch = fetch2;
766
+ this._apiConn?.setFetch(fetch2);
767
+ this._appConn?.setFetch(fetch2);
764
768
  }
765
769
  async login(loginParams) {
766
770
  if (this.apiUrl && !loginParams.forceLogin) {
@@ -849,15 +853,15 @@ var HTTPConnection = class _HTTPConnection {
849
853
  token;
850
854
  headers;
851
855
  fetch;
852
- constructor(base_url, fetch) {
856
+ constructor(base_url, fetch2) {
853
857
  this.base_url = base_url;
854
858
  this.token = null;
855
859
  this.headers = {};
856
860
  this._reset();
857
- this.fetch = fetch;
861
+ this.fetch = fetch2;
858
862
  }
859
- setFetch(fetch) {
860
- this.fetch = fetch;
863
+ setFetch(fetch2) {
864
+ this.fetch = fetch2;
861
865
  }
862
866
  async ping() {
863
867
  try {
@@ -893,12 +897,14 @@ var HTTPConnection = class _HTTPConnection {
893
897
  ([k, v]) => v !== void 0 ? typeof v === "string" ? [[k, v]] : v.map((x) => [k, x]) : []
894
898
  ) : []
895
899
  ).toString();
900
+ const this_fetch = this.fetch;
901
+ const this_headers = this.headers;
896
902
  return await checkResponse(
897
903
  // Using toString() here makes it work with isomorphic fetch
898
- await this.fetch(url.toString(), {
904
+ await this_fetch(url.toString(), {
899
905
  headers: {
900
906
  Accept: "application/json",
901
- ...this.headers,
907
+ ...this_headers,
902
908
  ...headers
903
909
  },
904
910
  keepalive: true,
@@ -908,13 +914,16 @@ var HTTPConnection = class _HTTPConnection {
908
914
  }
909
915
  async post(path3, params, config) {
910
916
  const { headers, ...rest } = config || {};
917
+ const this_fetch = this.fetch;
918
+ const this_base_url = this.base_url;
919
+ const this_headers = this.headers;
911
920
  return await checkResponse(
912
- await this.fetch(_urljoin(this.base_url, path3), {
921
+ await this_fetch(_urljoin(this_base_url, path3), {
913
922
  method: "POST",
914
923
  headers: {
915
924
  Accept: "application/json",
916
925
  "Content-Type": "application/json",
917
- ...this.headers,
926
+ ...this_headers,
918
927
  ...headers
919
928
  },
920
929
  body: typeof params === "string" ? params : params ? JSON.stringify(params) : void 0,
@@ -947,6 +956,167 @@ var HTTPConnection = class _HTTPConnection {
947
956
  return await resp.json();
948
957
  }
949
958
  };
959
+ var Attachment = class {
960
+ /**
961
+ * The object that replaces this `Attachment` at upload time.
962
+ */
963
+ reference;
964
+ uploader;
965
+ data;
966
+ state;
967
+ // For debug logging only.
968
+ dataDebugString;
969
+ /**
970
+ * Construct an attachment.
971
+ *
972
+ * @param data A string representing the path of the file on disk, or a
973
+ * `Blob`/`ArrayBuffer` with the file's contents. The caller is responsible
974
+ * for ensuring the file/blob/buffer is not modified until upload is complete.
975
+ *
976
+ * @param filename The desired name of the file in Braintrust after uploading.
977
+ * This parameter is for visualization purposes only and has no effect on
978
+ * attachment storage.
979
+ *
980
+ * @param contentType The MIME type of the file.
981
+ *
982
+ * @param state (Optional) For internal use.
983
+ */
984
+ constructor({ data, filename, contentType, state }) {
985
+ this.reference = {
986
+ type: BRAINTRUST_ATTACHMENT,
987
+ filename,
988
+ content_type: contentType,
989
+ key: newId()
990
+ };
991
+ this.state = state;
992
+ this.dataDebugString = typeof data === "string" ? data : "<in-memory data>";
993
+ this.data = this.initData(data);
994
+ this.uploader = this.initUploader();
995
+ }
996
+ /**
997
+ * On first access, (1) reads the attachment from disk if needed, (2)
998
+ * authenticates with the data plane to request a signed URL, (3) uploads to
999
+ * object store, and (4) updates the attachment.
1000
+ *
1001
+ * @returns The attachment status.
1002
+ */
1003
+ async upload() {
1004
+ return await this.uploader.get();
1005
+ }
1006
+ /**
1007
+ * A human-readable description for logging and debugging.
1008
+ *
1009
+ * @returns The debug object. The return type is not stable and may change in
1010
+ * a future release.
1011
+ */
1012
+ debugInfo() {
1013
+ return {
1014
+ inputData: this.dataDebugString,
1015
+ reference: this.reference,
1016
+ state: this.state
1017
+ };
1018
+ }
1019
+ initUploader() {
1020
+ const doUpload = async (conn, orgId) => {
1021
+ const requestParams = {
1022
+ key: this.reference.key,
1023
+ filename: this.reference.filename,
1024
+ content_type: this.reference.content_type,
1025
+ org_id: orgId
1026
+ };
1027
+ const [metadataPromiseResult, dataPromiseResult] = await Promise.allSettled([
1028
+ conn.post("/attachment", requestParams),
1029
+ this.data.get()
1030
+ ]);
1031
+ if (metadataPromiseResult.status === "rejected") {
1032
+ const errorStr = JSON.stringify(metadataPromiseResult.reason);
1033
+ throw new Error(
1034
+ `Failed to request signed URL from API server: ${errorStr}`
1035
+ );
1036
+ }
1037
+ if (dataPromiseResult.status === "rejected") {
1038
+ const errorStr = JSON.stringify(dataPromiseResult.reason);
1039
+ throw new Error(`Failed to read file: ${errorStr}`);
1040
+ }
1041
+ const metadataResponse = metadataPromiseResult.value;
1042
+ const data = dataPromiseResult.value;
1043
+ let signedUrl;
1044
+ let headers;
1045
+ try {
1046
+ ({ signedUrl, headers } = z2.object({
1047
+ signedUrl: z2.string().url(),
1048
+ headers: z2.record(z2.string())
1049
+ }).parse(await metadataResponse.json()));
1050
+ } catch (error2) {
1051
+ if (error2 instanceof ZodError) {
1052
+ const errorStr = JSON.stringify(error2.flatten());
1053
+ throw new Error(`Invalid response from API server: ${errorStr}`);
1054
+ }
1055
+ throw error2;
1056
+ }
1057
+ let objectStoreResponse;
1058
+ try {
1059
+ objectStoreResponse = await checkResponse(
1060
+ await fetch(signedUrl, {
1061
+ method: "PUT",
1062
+ headers,
1063
+ body: data
1064
+ })
1065
+ );
1066
+ } catch (error2) {
1067
+ if (error2 instanceof FailedHTTPResponse) {
1068
+ throw new Error(
1069
+ `Failed to upload attachment to object store: ${error2.status} ${error2.text} ${error2.data}`
1070
+ );
1071
+ }
1072
+ throw error2;
1073
+ }
1074
+ return { signedUrl, metadataResponse, objectStoreResponse };
1075
+ };
1076
+ const errorWrapper = async () => {
1077
+ const status = { upload_status: "done" };
1078
+ const state = this.state ?? _globalState;
1079
+ await state.login({});
1080
+ const conn = state.apiConn();
1081
+ const orgId = state.orgId ?? "";
1082
+ try {
1083
+ await doUpload(conn, orgId);
1084
+ } catch (error2) {
1085
+ status.upload_status = "error";
1086
+ status.error_message = error2 instanceof Error ? error2.message : JSON.stringify(error2);
1087
+ }
1088
+ const requestParams = {
1089
+ key: this.reference.key,
1090
+ org_id: orgId,
1091
+ status
1092
+ };
1093
+ const statusResponse = await conn.post(
1094
+ "/attachment/status",
1095
+ requestParams
1096
+ );
1097
+ if (!statusResponse.ok) {
1098
+ const errorStr = JSON.stringify(statusResponse);
1099
+ throw new Error(`Couldn't log attachment status: ${errorStr}`);
1100
+ }
1101
+ return status;
1102
+ };
1103
+ return new LazyValue(errorWrapper);
1104
+ }
1105
+ initData(data) {
1106
+ if (typeof data === "string") {
1107
+ const readFile2 = isomorph_default.readFile;
1108
+ if (!readFile2) {
1109
+ throw new Error(
1110
+ `This platform does not support reading the filesystem. Construct the Attachment
1111
+ with a Blob/ArrayBuffer, or run the program on Node.js.`
1112
+ );
1113
+ }
1114
+ return new LazyValue(async () => new Blob([await readFile2(data)]));
1115
+ } else {
1116
+ return new LazyValue(async () => new Blob([data]));
1117
+ }
1118
+ }
1119
+ };
950
1120
  function logFeedbackImpl(state, parentObjectType, parentObjectId, {
951
1121
  id,
952
1122
  expected,
@@ -971,7 +1141,7 @@ function logFeedbackImpl(state, parentObjectType, parentObjectId, {
971
1141
  expected,
972
1142
  tags
973
1143
  });
974
- let { metadata, ...updateEvent } = validatedEvent;
1144
+ let { metadata, ...updateEvent } = deepCopyEvent(validatedEvent);
975
1145
  updateEvent = Object.fromEntries(
976
1146
  Object.entries(updateEvent).filter(([_, v]) => !isEmpty(v))
977
1147
  );
@@ -1020,10 +1190,12 @@ function updateSpanImpl({
1020
1190
  id,
1021
1191
  event
1022
1192
  }) {
1023
- const updateEvent = validateAndSanitizeExperimentLogPartialArgs({
1024
- id,
1025
- ...event
1026
- });
1193
+ const updateEvent = deepCopyEvent(
1194
+ validateAndSanitizeExperimentLogPartialArgs({
1195
+ id,
1196
+ ...event
1197
+ })
1198
+ );
1027
1199
  const parentIds = async () => new SpanComponentsV3({
1028
1200
  object_type: parentObjectType,
1029
1201
  object_id: await parentObjectId.get()
@@ -1224,7 +1396,7 @@ var Logger = class {
1224
1396
  * @param event.id: (Optional) a unique identifier for the event. If you don't provide one, BrainTrust will generate one for you.
1225
1397
  * @param options Additional logging options
1226
1398
  * @param options.allowConcurrentWithSpans in rare cases where you need to log at the top level separately from spans on the logger elsewhere, set this to true.
1227
- * :returns: The `id` of the logged event.
1399
+ * @returns The `id` of the logged event.
1228
1400
  */
1229
1401
  log(event, options) {
1230
1402
  if (this.calledStartSpan && !options?.allowConcurrentWithSpans) {
@@ -1247,7 +1419,7 @@ var Logger = class {
1247
1419
  /**
1248
1420
  * Create a new toplevel span underneath the logger. The name defaults to "root".
1249
1421
  *
1250
- * See `Span.traced` for full details.
1422
+ * See {@link Span.traced} for full details.
1251
1423
  */
1252
1424
  traced(callback, args) {
1253
1425
  const { setCurrent, ...argsRest } = args ?? {};
@@ -1281,7 +1453,7 @@ var Logger = class {
1281
1453
  * where you cannot use callbacks. However, spans started with `startSpan` will not be marked as the "current span",
1282
1454
  * so `currentSpan()` and `traced()` will be no-ops. If you want to mark a span as current, use `traced` instead.
1283
1455
  *
1284
- * See `traced` for full details.
1456
+ * See {@link traced} for full details.
1285
1457
  */
1286
1458
  startSpan(args) {
1287
1459
  this.calledStartSpan = true;
@@ -1321,7 +1493,7 @@ var Logger = class {
1321
1493
  * Update a span in the experiment using its id. It is important that you only update a span once the original span has been fully written and flushed,
1322
1494
  * since otherwise updates to the span may conflict with the original span.
1323
1495
  *
1324
- * @param event The event data to update the span with. Must include `id`. See `Experiment.log` for a full list of valid fields.
1496
+ * @param event The event data to update the span with. Must include `id`. See {@link Experiment.log} for a full list of valid fields.
1325
1497
  */
1326
1498
  updateSpan(event) {
1327
1499
  const { id, ...eventRest } = event;
@@ -1337,7 +1509,9 @@ var Logger = class {
1337
1509
  });
1338
1510
  }
1339
1511
  /**
1340
- * Return a serialized representation of the logger that can be used to start subspans in other places. See `Span.start_span` for more details.
1512
+ * Return a serialized representation of the logger that can be used to start subspans in other places.
1513
+ *
1514
+ * See {@link Span.startSpan} for more details.
1341
1515
  */
1342
1516
  async export() {
1343
1517
  return new SpanComponentsV3({
@@ -1377,6 +1551,7 @@ var BackgroundLogger = class _BackgroundLogger {
1377
1551
  activeFlush = Promise.resolve();
1378
1552
  activeFlushResolved = true;
1379
1553
  activeFlushError = void 0;
1554
+ onFlushError;
1380
1555
  syncFlush = false;
1381
1556
  // 6 MB for the AWS lambda gateway (from our own testing).
1382
1557
  maxRequestSize = 6 * 1024 * 1024;
@@ -1440,6 +1615,7 @@ var BackgroundLogger = class _BackgroundLogger {
1440
1615
  await this.flush();
1441
1616
  });
1442
1617
  }
1618
+ this.onFlushError = opts.onFlushError;
1443
1619
  }
1444
1620
  log(items) {
1445
1621
  const [addedItems, droppedItems] = (() => {
@@ -1471,14 +1647,16 @@ var BackgroundLogger = class _BackgroundLogger {
1471
1647
  if (this.activeFlushError) {
1472
1648
  const err = this.activeFlushError;
1473
1649
  this.activeFlushError = void 0;
1474
- throw err;
1650
+ if (this.syncFlush) {
1651
+ throw err;
1652
+ }
1475
1653
  }
1476
1654
  }
1477
1655
  async flushOnce(args) {
1478
1656
  const batchSize = args?.batchSize ?? this.defaultBatchSize;
1479
1657
  const wrappedItems = this.items;
1480
1658
  this.items = [];
1481
- const allItems = await this.unwrapLazyValues(wrappedItems);
1659
+ const [allItems, attachments] = await this.unwrapLazyValues(wrappedItems);
1482
1660
  if (allItems.length === 0) {
1483
1661
  return;
1484
1662
  }
@@ -1510,6 +1688,23 @@ var BackgroundLogger = class _BackgroundLogger {
1510
1688
  );
1511
1689
  }
1512
1690
  }
1691
+ const attachmentErrors = [];
1692
+ for (const attachment of attachments) {
1693
+ try {
1694
+ const result = await attachment.upload();
1695
+ if (result.upload_status === "error" && result.error_message) {
1696
+ attachmentErrors.push(new Error(result.error_message));
1697
+ }
1698
+ } catch (error2) {
1699
+ attachmentErrors.push(error2);
1700
+ }
1701
+ }
1702
+ if (attachmentErrors.length > 0) {
1703
+ throw new AggregateError(
1704
+ attachmentErrors,
1705
+ `Encountered the following errors while uploading attachments:`
1706
+ );
1707
+ }
1513
1708
  if (this.items.length > 0) {
1514
1709
  await this.flushOnce(args);
1515
1710
  }
@@ -1517,8 +1712,10 @@ var BackgroundLogger = class _BackgroundLogger {
1517
1712
  async unwrapLazyValues(wrappedItems) {
1518
1713
  for (let i = 0; i < this.numTries; ++i) {
1519
1714
  try {
1520
- const itemPromises = wrappedItems.map((x) => x.get());
1521
- return mergeRowBatch(await Promise.all(itemPromises));
1715
+ const items = await Promise.all(wrappedItems.map((x) => x.get()));
1716
+ const attachments = [];
1717
+ items.forEach((item) => extractAttachments(item, attachments));
1718
+ return [mergeRowBatch(items), attachments];
1522
1719
  } catch (e) {
1523
1720
  let errmsg = "Encountered error when constructing records to flush";
1524
1721
  const isRetrying = i + 1 < this.numTries;
@@ -1526,7 +1723,10 @@ var BackgroundLogger = class _BackgroundLogger {
1526
1723
  errmsg += ". Retrying";
1527
1724
  }
1528
1725
  console.warn(errmsg);
1529
- if (!isRetrying && this.syncFlush) {
1726
+ if (!isRetrying) {
1727
+ console.warn(
1728
+ `Failed to construct log records to flush after ${this.numTries} attempts. Dropping batch`
1729
+ );
1530
1730
  throw e;
1531
1731
  } else {
1532
1732
  console.warn(e);
@@ -1534,10 +1734,7 @@ var BackgroundLogger = class _BackgroundLogger {
1534
1734
  }
1535
1735
  }
1536
1736
  }
1537
- console.warn(
1538
- `Failed to construct log records to flush after ${this.numTries} attempts. Dropping batch`
1539
- );
1540
- return [];
1737
+ throw new Error("Impossible");
1541
1738
  }
1542
1739
  async submitLogsRequest(items) {
1543
1740
  const conn = await this.apiConn.get();
@@ -1553,16 +1750,14 @@ var BackgroundLogger = class _BackgroundLogger {
1553
1750
  let error2 = void 0;
1554
1751
  try {
1555
1752
  await conn.post_json("logs3", dataStr);
1556
- } catch (e) {
1753
+ } catch {
1557
1754
  try {
1558
1755
  const legacyDataS = constructJsonArray(
1559
- items.map(
1560
- (r) => JSON.stringify(makeLegacyEvent(JSON.parse(r)))
1561
- )
1756
+ items.map((r) => JSON.stringify(makeLegacyEvent(JSON.parse(r))))
1562
1757
  );
1563
1758
  await conn.post_json("logs", legacyDataS);
1564
- } catch (e2) {
1565
- error2 = e2;
1759
+ } catch (e) {
1760
+ error2 = e;
1566
1761
  }
1567
1762
  }
1568
1763
  if (error2 === void 0) {
@@ -1586,7 +1781,10 @@ Error: ${errorText}`;
1586
1781
  });
1587
1782
  this.logFailedPayloadsDir();
1588
1783
  }
1589
- if (!isRetrying && this.syncFlush) {
1784
+ if (!isRetrying) {
1785
+ console.warn(
1786
+ `log request failed after ${this.numTries} retries. Dropping batch`
1787
+ );
1590
1788
  throw new Error(errMsg);
1591
1789
  } else {
1592
1790
  console.warn(errMsg);
@@ -1595,10 +1793,6 @@ Error: ${errorText}`;
1595
1793
  }
1596
1794
  }
1597
1795
  }
1598
- console.warn(
1599
- `log request failed after ${this.numTries} retries. Dropping batch`
1600
- );
1601
- return;
1602
1796
  }
1603
1797
  registerDroppedItemCount(numItems) {
1604
1798
  if (numItems <= 0) {
@@ -1626,15 +1820,17 @@ Error: ${errorText}`;
1626
1820
  return;
1627
1821
  }
1628
1822
  try {
1629
- const allItems = await this.unwrapLazyValues(wrappedItems);
1823
+ const [allItems, allAttachments] = await this.unwrapLazyValues(wrappedItems);
1630
1824
  const dataStr = constructLogs3Data(
1631
1825
  allItems.map((x) => JSON.stringify(x))
1632
1826
  );
1827
+ const attachmentStr = JSON.stringify(
1828
+ allAttachments.map((a) => a.debugInfo())
1829
+ );
1830
+ const payload = `{"data": ${dataStr}, "attachments": ${attachmentStr}}
1831
+ `;
1633
1832
  for (const payloadDir of publishPayloadsDir) {
1634
- await _BackgroundLogger.writePayloadToDir({
1635
- payloadDir,
1636
- payload: dataStr
1637
- });
1833
+ await _BackgroundLogger.writePayloadToDir({ payloadDir, payload });
1638
1834
  }
1639
1835
  } catch (e) {
1640
1836
  console.error(e);
@@ -1673,6 +1869,13 @@ Error: ${errorText}`;
1673
1869
  try {
1674
1870
  await this.flushOnce();
1675
1871
  } catch (err) {
1872
+ if (err instanceof AggregateError) {
1873
+ for (const e of err.errors) {
1874
+ this.onFlushError?.(e);
1875
+ }
1876
+ } else {
1877
+ this.onFlushError?.(err);
1878
+ }
1676
1879
  this.activeFlushError = err;
1677
1880
  } finally {
1678
1881
  this.activeFlushResolved = true;
@@ -1715,7 +1918,7 @@ function init(projectOrOptions, optionalOptions) {
1715
1918
  apiKey,
1716
1919
  orgName,
1717
1920
  forceLogin,
1718
- fetch,
1921
+ fetch: fetch2,
1719
1922
  metadata,
1720
1923
  gitMetadataSettings,
1721
1924
  projectId,
@@ -1733,7 +1936,7 @@ function init(projectOrOptions, optionalOptions) {
1733
1936
  }
1734
1937
  const lazyMetadata2 = new LazyValue(
1735
1938
  async () => {
1736
- await state.login({ apiKey, appUrl, orgName, fetch, forceLogin });
1939
+ await state.login({ apiKey, appUrl, orgName, fetch: fetch2, forceLogin });
1737
1940
  const args = {
1738
1941
  project_name: project,
1739
1942
  project_id: projectId,
@@ -1904,7 +2107,7 @@ function initDataset(projectOrOptions, optionalOptions) {
1904
2107
  appUrl,
1905
2108
  apiKey,
1906
2109
  orgName,
1907
- fetch,
2110
+ fetch: fetch2,
1908
2111
  forceLogin,
1909
2112
  projectId,
1910
2113
  metadata,
@@ -1918,7 +2121,7 @@ function initDataset(projectOrOptions, optionalOptions) {
1918
2121
  orgName,
1919
2122
  apiKey,
1920
2123
  appUrl,
1921
- fetch,
2124
+ fetch: fetch2,
1922
2125
  forceLogin
1923
2126
  });
1924
2127
  const args = {
@@ -2000,7 +2203,7 @@ function initLogger(options = {}) {
2000
2203
  apiKey,
2001
2204
  orgName,
2002
2205
  forceLogin,
2003
- fetch,
2206
+ fetch: fetch2,
2004
2207
  state: stateArg
2005
2208
  } = options || {};
2006
2209
  const computeMetadataArgs = {
@@ -2015,7 +2218,7 @@ function initLogger(options = {}) {
2015
2218
  apiKey,
2016
2219
  appUrl,
2017
2220
  forceLogin,
2018
- fetch
2221
+ fetch: fetch2
2019
2222
  });
2020
2223
  return computeLoggerMetadata(state, computeMetadataArgs);
2021
2224
  }
@@ -2039,7 +2242,7 @@ async function loadPrompt({
2039
2242
  appUrl,
2040
2243
  apiKey,
2041
2244
  orgName,
2042
- fetch,
2245
+ fetch: fetch2,
2043
2246
  forceLogin,
2044
2247
  state: stateArg
2045
2248
  }) {
@@ -2054,7 +2257,7 @@ async function loadPrompt({
2054
2257
  orgName,
2055
2258
  apiKey,
2056
2259
  appUrl,
2057
- fetch,
2260
+ fetch: fetch2,
2058
2261
  forceLogin
2059
2262
  });
2060
2263
  const args = {
@@ -2105,7 +2308,7 @@ async function loginToState(options = {}) {
2105
2308
  appUrl = isomorph_default.getEnv("BRAINTRUST_APP_URL") || "https://www.braintrust.dev",
2106
2309
  apiKey = isomorph_default.getEnv("BRAINTRUST_API_KEY"),
2107
2310
  orgName = isomorph_default.getEnv("BRAINTRUST_ORG_NAME"),
2108
- fetch = globalThis.fetch
2311
+ fetch: fetch2 = globalThis.fetch
2109
2312
  } = options || {};
2110
2313
  const appPublicUrl = isomorph_default.getEnv("BRAINTRUST_APP_PUBLIC_URL") || appUrl;
2111
2314
  const state = new BraintrustState(options);
@@ -2115,7 +2318,7 @@ async function loginToState(options = {}) {
2115
2318
  let conn = null;
2116
2319
  if (apiKey !== void 0) {
2117
2320
  const resp = await checkResponse(
2118
- await fetch(_urljoin(state.appUrl, `/api/apikey/login`), {
2321
+ await fetch2(_urljoin(state.appUrl, `/api/apikey/login`), {
2119
2322
  method: "POST",
2120
2323
  headers: {
2121
2324
  "Content-Type": "application/json",
@@ -2283,8 +2486,8 @@ async function flush(options) {
2283
2486
  const state = options?.state ?? _globalState;
2284
2487
  return await state.bgLogger().flush();
2285
2488
  }
2286
- function setFetch(fetch) {
2287
- _globalState.setFetch(fetch);
2489
+ function setFetch(fetch2) {
2490
+ _globalState.setFetch(fetch2);
2288
2491
  }
2289
2492
  function startSpanAndIsLogger(args) {
2290
2493
  const state = args?.state ?? _globalState;
@@ -2412,6 +2615,47 @@ function validateAndSanitizeExperimentLogPartialArgs(event) {
2412
2615
  return { ...event };
2413
2616
  }
2414
2617
  }
2618
+ function deepCopyEvent(event) {
2619
+ const attachments = [];
2620
+ const IDENTIFIER = "_bt_internal_saved_attachment";
2621
+ const savedAttachmentSchema = z2.strictObject({ [IDENTIFIER]: z2.number() });
2622
+ const serialized = JSON.stringify(event, (_k, v) => {
2623
+ if (v instanceof SpanImpl || v instanceof NoopSpan) {
2624
+ return `<span>`;
2625
+ } else if (v instanceof Experiment) {
2626
+ return `<experiment>`;
2627
+ } else if (v instanceof Dataset) {
2628
+ return `<dataset>`;
2629
+ } else if (v instanceof Logger) {
2630
+ return `<logger>`;
2631
+ } else if (v instanceof Attachment) {
2632
+ const idx = attachments.push(v);
2633
+ return { [IDENTIFIER]: idx - 1 };
2634
+ }
2635
+ return v;
2636
+ });
2637
+ const x = JSON.parse(serialized, (_k, v) => {
2638
+ const parsedAttachment = savedAttachmentSchema.safeParse(v);
2639
+ if (parsedAttachment.success) {
2640
+ return attachments[parsedAttachment.data[IDENTIFIER]];
2641
+ }
2642
+ return v;
2643
+ });
2644
+ return x;
2645
+ }
2646
+ function extractAttachments(event, attachments) {
2647
+ for (const [key, value] of Object.entries(event)) {
2648
+ if (value instanceof Attachment) {
2649
+ attachments.push(value);
2650
+ event[key] = value.reference;
2651
+ continue;
2652
+ }
2653
+ if (!(value instanceof Object)) {
2654
+ continue;
2655
+ }
2656
+ extractAttachments(value, attachments);
2657
+ }
2658
+ }
2415
2659
  function validateAndSanitizeExperimentLogFullArgs(event, hasDataset) {
2416
2660
  if ("input" in event && !isEmpty(event.input) && "inputs" in event && !isEmpty(event.inputs) || !("input" in event) && !("inputs" in event)) {
2417
2661
  throw new Error(
@@ -2542,10 +2786,9 @@ var Experiment = class extends ObjectFetcher {
2542
2786
  * @param event.metrics: (Optional) a dictionary of metrics to log. The following keys are populated automatically: "start", "end".
2543
2787
  * @param event.id: (Optional) a unique identifier for the event. If you don't provide one, BrainTrust will generate one for you.
2544
2788
  * @param event.dataset_record_id: (Optional) the id of the dataset record that this event is associated with. This field is required if and only if the experiment is associated with a dataset.
2545
- * @param event.inputs: (Deprecated) the same as `input` (will be removed in a future version).
2546
2789
  * @param options Additional logging options
2547
2790
  * @param options.allowConcurrentWithSpans in rare cases where you need to log at the top level separately from spans on the experiment elsewhere, set this to true.
2548
- * :returns: The `id` of the logged event.
2791
+ * @returns The `id` of the logged event.
2549
2792
  */
2550
2793
  log(event, options) {
2551
2794
  if (this.calledStartSpan && !options?.allowConcurrentWithSpans) {
@@ -2561,7 +2804,7 @@ var Experiment = class extends ObjectFetcher {
2561
2804
  /**
2562
2805
  * Create a new toplevel span underneath the experiment. The name defaults to "root".
2563
2806
  *
2564
- * See `Span.traced` for full details.
2807
+ * See {@link Span.traced} for full details.
2565
2808
  */
2566
2809
  traced(callback, args) {
2567
2810
  const { setCurrent, ...argsRest } = args ?? {};
@@ -2587,7 +2830,7 @@ var Experiment = class extends ObjectFetcher {
2587
2830
  * where you cannot use callbacks. However, spans started with `startSpan` will not be marked as the "current span",
2588
2831
  * so `currentSpan()` and `traced()` will be no-ops. If you want to mark a span as current, use `traced` instead.
2589
2832
  *
2590
- * See `traced` for full details.
2833
+ * See {@link traced} for full details.
2591
2834
  */
2592
2835
  startSpan(args) {
2593
2836
  this.calledStartSpan = true;
@@ -2699,7 +2942,7 @@ var Experiment = class extends ObjectFetcher {
2699
2942
  * Update a span in the experiment using its id. It is important that you only update a span once the original span has been fully written and flushed,
2700
2943
  * since otherwise updates to the span may conflict with the original span.
2701
2944
  *
2702
- * @param event The event data to update the span with. Must include `id`. See `Experiment.log` for a full list of valid fields.
2945
+ * @param event The event data to update the span with. Must include `id`. See {@link Experiment.log} for a full list of valid fields.
2703
2946
  */
2704
2947
  updateSpan(event) {
2705
2948
  const { id, ...eventRest } = event;
@@ -2715,7 +2958,9 @@ var Experiment = class extends ObjectFetcher {
2715
2958
  });
2716
2959
  }
2717
2960
  /**
2718
- * Return a serialized representation of the experiment that can be used to start subspans in other places. See `Span.start_span` for more details.
2961
+ * Return a serialized representation of the experiment that can be used to start subspans in other places.
2962
+ *
2963
+ * See {@link Span.startSpan} for more details.
2719
2964
  */
2720
2965
  async export() {
2721
2966
  return new SpanComponentsV3({
@@ -2730,7 +2975,7 @@ var Experiment = class extends ObjectFetcher {
2730
2975
  return await this.state.bgLogger().flush();
2731
2976
  }
2732
2977
  /**
2733
- * This function is deprecated. You can simply remove it from your code.
2978
+ * @deprecated This function is deprecated. You can simply remove it from your code.
2734
2979
  */
2735
2980
  async close() {
2736
2981
  console.warn(
@@ -2872,27 +3117,14 @@ var SpanImpl = class _SpanImpl {
2872
3117
  event,
2873
3118
  internalData
2874
3119
  });
2875
- let partialRecord = {
3120
+ const partialRecord = deepCopyEvent({
2876
3121
  id: this.id,
2877
3122
  span_id: this.spanId,
2878
3123
  root_span_id: this.rootSpanId,
2879
3124
  span_parents: this.spanParents,
2880
3125
  ...serializableInternalData,
2881
3126
  [IS_MERGE_FIELD]: this.isMerge
2882
- };
2883
- const serializedPartialRecord = JSON.stringify(partialRecord, (_k, v) => {
2884
- if (v instanceof _SpanImpl) {
2885
- return `<span>`;
2886
- } else if (v instanceof Experiment) {
2887
- return `<experiment>`;
2888
- } else if (v instanceof Dataset) {
2889
- return `<dataset>`;
2890
- } else if (v instanceof Logger) {
2891
- return `<logger>`;
2892
- }
2893
- return v;
2894
3127
  });
2895
- partialRecord = JSON.parse(serializedPartialRecord);
2896
3128
  if (partialRecord.metrics?.end) {
2897
3129
  this.loggedEndTime = partialRecord.metrics?.end;
2898
3130
  }
@@ -2978,6 +3210,11 @@ var SpanImpl = class _SpanImpl {
2978
3210
  propagated_event: this.propagatedEvent
2979
3211
  }).toStr();
2980
3212
  }
3213
+ async permalink() {
3214
+ return await permalink(await this.export(), {
3215
+ state: this.state
3216
+ });
3217
+ }
2981
3218
  async flush() {
2982
3219
  return await this.state.bgLogger().flush();
2983
3220
  }
@@ -3127,15 +3364,17 @@ var Dataset = class extends ObjectFetcher {
3127
3364
  }) {
3128
3365
  this.validateEvent({ metadata, expected, output, tags });
3129
3366
  const rowId = id || uuidv4();
3130
- const args = this.createArgs({
3131
- id: rowId,
3132
- input,
3133
- expected,
3134
- metadata,
3135
- tags,
3136
- output,
3137
- isMerge: false
3138
- });
3367
+ const args = this.createArgs(
3368
+ deepCopyEvent({
3369
+ id: rowId,
3370
+ input,
3371
+ expected,
3372
+ metadata,
3373
+ tags,
3374
+ output,
3375
+ isMerge: false
3376
+ })
3377
+ );
3139
3378
  this.state.bgLogger().log([args]);
3140
3379
  return rowId;
3141
3380
  }
@@ -3160,14 +3399,16 @@ var Dataset = class extends ObjectFetcher {
3160
3399
  id
3161
3400
  }) {
3162
3401
  this.validateEvent({ metadata, expected, tags });
3163
- const args = this.createArgs({
3164
- id,
3165
- input,
3166
- expected,
3167
- metadata,
3168
- tags,
3169
- isMerge: true
3170
- });
3402
+ const args = this.createArgs(
3403
+ deepCopyEvent({
3404
+ id,
3405
+ input,
3406
+ expected,
3407
+ metadata,
3408
+ tags,
3409
+ isMerge: true
3410
+ })
3411
+ );
3171
3412
  this.state.bgLogger().log([args]);
3172
3413
  return id;
3173
3414
  }
@@ -3222,7 +3463,7 @@ var Dataset = class extends ObjectFetcher {
3222
3463
  return await this.state.bgLogger().flush();
3223
3464
  }
3224
3465
  /**
3225
- * This function is deprecated. You can simply remove it from your code.
3466
+ * @deprecated This function is deprecated. You can simply remove it from your code.
3226
3467
  */
3227
3468
  async close() {
3228
3469
  console.warn(
@@ -3386,6 +3627,7 @@ var Prompt = class {
3386
3627
  return this.parsedPromptData;
3387
3628
  }
3388
3629
  };
3630
+ var _exportsForTestingOnly = { extractAttachments, deepCopyEvent };
3389
3631
 
3390
3632
  // src/node.ts
3391
3633
  function configureNode() {
@@ -3401,12 +3643,14 @@ function configureNode() {
3401
3643
  isomorph_default.pathDirname = path.dirname;
3402
3644
  isomorph_default.mkdir = fs.mkdir;
3403
3645
  isomorph_default.writeFile = fs.writeFile;
3646
+ isomorph_default.readFile = fs.readFile;
3404
3647
  _internalSetInitialState();
3405
3648
  }
3406
3649
 
3407
3650
  // src/exports-node.ts
3408
3651
  var exports_node_exports = {};
3409
3652
  __export(exports_node_exports, {
3653
+ Attachment: () => Attachment,
3410
3654
  BaseExperiment: () => BaseExperiment,
3411
3655
  BraintrustState: () => BraintrustState,
3412
3656
  BraintrustStream: () => BraintrustStream,
@@ -3429,6 +3673,7 @@ __export(exports_node_exports, {
3429
3673
  SpanImpl: () => SpanImpl,
3430
3674
  ToolBuilder: () => ToolBuilder,
3431
3675
  X_CACHED_HEADER: () => X_CACHED_HEADER,
3676
+ _exportsForTestingOnly: () => _exportsForTestingOnly,
3432
3677
  _internalGetGlobalState: () => _internalGetGlobalState,
3433
3678
  _internalSetInitialState: () => _internalSetInitialState,
3434
3679
  braintrustStreamChunkSchema: () => braintrustStreamChunkSchema,
@@ -3484,7 +3729,7 @@ async function invoke(args) {
3484
3729
  apiKey,
3485
3730
  appUrl,
3486
3731
  forceLogin,
3487
- fetch,
3732
+ fetch: fetch2,
3488
3733
  input,
3489
3734
  messages,
3490
3735
  parent: parentArg,
@@ -3500,7 +3745,7 @@ async function invoke(args) {
3500
3745
  apiKey,
3501
3746
  appUrl,
3502
3747
  forceLogin,
3503
- fetch
3748
+ fetch: fetch2
3504
3749
  });
3505
3750
  const parent = parentArg ? typeof parentArg === "string" ? parentArg : await parentArg.export() : await getSpanParentObject().export();
3506
3751
  const functionId = functionIdSchema.safeParse({
@@ -4751,6 +4996,7 @@ async function Eval(name, evaluator, reporterOrOpts) {
4751
4996
  }
4752
4997
  if (globalThis._lazy_load) {
4753
4998
  globalThis._evals.evaluators[evalName] = {
4999
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
4754
5000
  evaluator: {
4755
5001
  evalName,
4756
5002
  projectName: name,
@@ -4789,7 +5035,8 @@ async function Eval(name, evaluator, reporterOrOpts) {
4789
5035
  baseExperiment: evaluator.baseExperimentName ?? defaultBaseExperiment,
4790
5036
  baseExperimentId: evaluator.baseExperimentId,
4791
5037
  gitMetadataSettings: evaluator.gitMetadataSettings,
4792
- repoInfo: evaluator.repoInfo
5038
+ repoInfo: evaluator.repoInfo,
5039
+ dataset: data instanceof Dataset ? data : void 0
4793
5040
  });
4794
5041
  if (options.onStart) {
4795
5042
  experiment.summarize({ summarizeScores: false }).then(options.onStart);
@@ -4835,7 +5082,10 @@ function serializeJSONWithPlainString(v) {
4835
5082
  function evaluateFilter(object, filter2) {
4836
5083
  const { path: path3, pattern } = filter2;
4837
5084
  const key = path3.reduce(
4838
- (acc, p) => typeof acc === "object" && acc !== null ? acc[p] : void 0,
5085
+ (acc, p) => typeof acc === "object" && acc !== null ? (
5086
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
5087
+ acc[p]
5088
+ ) : void 0,
4839
5089
  object
4840
5090
  );
4841
5091
  if (key === void 0) {
@@ -4942,7 +5192,12 @@ async function runEvaluatorInternal(experiment, evaluator, progressReporter, fil
4942
5192
  }
4943
5193
  );
4944
5194
  rootSpan.log({ output, metadata });
4945
- const scoringArgs = { ...datum, metadata, output };
5195
+ const scoringArgs = {
5196
+ input: datum.input,
5197
+ expected: "expected" in datum ? datum.expected : void 0,
5198
+ metadata,
5199
+ output
5200
+ };
4946
5201
  const scorerNames = evaluator.scores.map(scorerName);
4947
5202
  const scoreResults = await Promise.all(
4948
5203
  evaluator.scores.map(async (score, score_idx) => {
@@ -5069,7 +5324,13 @@ async function runEvaluatorInternal(experiment, evaluator, progressReporter, fil
5069
5324
  event: {
5070
5325
  input: datum.input,
5071
5326
  expected: "expected" in datum ? datum.expected : void 0,
5072
- tags: datum.tags
5327
+ tags: datum.tags,
5328
+ origin: experiment.dataset && datum.id && datum._xact_id ? {
5329
+ object_type: "dataset",
5330
+ object_id: await experiment.dataset.id,
5331
+ id: datum.id,
5332
+ _xact_id: datum._xact_id
5333
+ } : void 0
5073
5334
  }
5074
5335
  });
5075
5336
  }
@@ -5931,6 +6192,7 @@ function postProcessOutput(text, tool_calls, finish_reason) {
5931
6192
  configureNode();
5932
6193
  var src_default = exports_node_exports;
5933
6194
  export {
6195
+ Attachment,
5934
6196
  BaseExperiment,
5935
6197
  BraintrustState,
5936
6198
  BraintrustStream,
@@ -5953,6 +6215,7 @@ export {
5953
6215
  SpanImpl,
5954
6216
  ToolBuilder,
5955
6217
  X_CACHED_HEADER,
6218
+ _exportsForTestingOnly,
5956
6219
  _internalGetGlobalState,
5957
6220
  _internalSetInitialState,
5958
6221
  braintrustStreamChunkSchema,