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.js CHANGED
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var src_exports = {};
32
32
  __export(src_exports, {
33
+ Attachment: () => Attachment,
33
34
  BaseExperiment: () => BaseExperiment,
34
35
  BraintrustState: () => BraintrustState,
35
36
  BraintrustStream: () => BraintrustStream,
@@ -52,6 +53,7 @@ __export(src_exports, {
52
53
  SpanImpl: () => SpanImpl,
53
54
  ToolBuilder: () => ToolBuilder,
54
55
  X_CACHED_HEADER: () => X_CACHED_HEADER,
56
+ _exportsForTestingOnly: () => _exportsForTestingOnly,
55
57
  _internalGetGlobalState: () => _internalGetGlobalState,
56
58
  _internalSetInitialState: () => _internalSetInitialState,
57
59
  braintrustStreamChunkSchema: () => braintrustStreamChunkSchema,
@@ -679,6 +681,9 @@ var NoopSpan = class {
679
681
  async export() {
680
682
  return "";
681
683
  }
684
+ async permalink() {
685
+ return "";
686
+ }
682
687
  async flush() {
683
688
  }
684
689
  close(args) {
@@ -820,11 +825,11 @@ var BraintrustState = class _BraintrustState {
820
825
  state.loginReplaceApiConn(state.apiConn());
821
826
  return state;
822
827
  }
823
- setFetch(fetch) {
824
- this.loginParams.fetch = fetch;
825
- this.fetch = fetch;
826
- this._apiConn?.setFetch(fetch);
827
- this._appConn?.setFetch(fetch);
828
+ setFetch(fetch2) {
829
+ this.loginParams.fetch = fetch2;
830
+ this.fetch = fetch2;
831
+ this._apiConn?.setFetch(fetch2);
832
+ this._appConn?.setFetch(fetch2);
828
833
  }
829
834
  async login(loginParams) {
830
835
  if (this.apiUrl && !loginParams.forceLogin) {
@@ -913,15 +918,15 @@ var HTTPConnection = class _HTTPConnection {
913
918
  token;
914
919
  headers;
915
920
  fetch;
916
- constructor(base_url, fetch) {
921
+ constructor(base_url, fetch2) {
917
922
  this.base_url = base_url;
918
923
  this.token = null;
919
924
  this.headers = {};
920
925
  this._reset();
921
- this.fetch = fetch;
926
+ this.fetch = fetch2;
922
927
  }
923
- setFetch(fetch) {
924
- this.fetch = fetch;
928
+ setFetch(fetch2) {
929
+ this.fetch = fetch2;
925
930
  }
926
931
  async ping() {
927
932
  try {
@@ -957,12 +962,14 @@ var HTTPConnection = class _HTTPConnection {
957
962
  ([k, v]) => v !== void 0 ? typeof v === "string" ? [[k, v]] : v.map((x) => [k, x]) : []
958
963
  ) : []
959
964
  ).toString();
965
+ const this_fetch = this.fetch;
966
+ const this_headers = this.headers;
960
967
  return await checkResponse(
961
968
  // Using toString() here makes it work with isomorphic fetch
962
- await this.fetch(url.toString(), {
969
+ await this_fetch(url.toString(), {
963
970
  headers: {
964
971
  Accept: "application/json",
965
- ...this.headers,
972
+ ...this_headers,
966
973
  ...headers
967
974
  },
968
975
  keepalive: true,
@@ -972,13 +979,16 @@ var HTTPConnection = class _HTTPConnection {
972
979
  }
973
980
  async post(path3, params, config) {
974
981
  const { headers, ...rest } = config || {};
982
+ const this_fetch = this.fetch;
983
+ const this_base_url = this.base_url;
984
+ const this_headers = this.headers;
975
985
  return await checkResponse(
976
- await this.fetch((0, import_core._urljoin)(this.base_url, path3), {
986
+ await this_fetch((0, import_core._urljoin)(this_base_url, path3), {
977
987
  method: "POST",
978
988
  headers: {
979
989
  Accept: "application/json",
980
990
  "Content-Type": "application/json",
981
- ...this.headers,
991
+ ...this_headers,
982
992
  ...headers
983
993
  },
984
994
  body: typeof params === "string" ? params : params ? JSON.stringify(params) : void 0,
@@ -1011,6 +1021,167 @@ var HTTPConnection = class _HTTPConnection {
1011
1021
  return await resp.json();
1012
1022
  }
1013
1023
  };
1024
+ var Attachment = class {
1025
+ /**
1026
+ * The object that replaces this `Attachment` at upload time.
1027
+ */
1028
+ reference;
1029
+ uploader;
1030
+ data;
1031
+ state;
1032
+ // For debug logging only.
1033
+ dataDebugString;
1034
+ /**
1035
+ * Construct an attachment.
1036
+ *
1037
+ * @param data A string representing the path of the file on disk, or a
1038
+ * `Blob`/`ArrayBuffer` with the file's contents. The caller is responsible
1039
+ * for ensuring the file/blob/buffer is not modified until upload is complete.
1040
+ *
1041
+ * @param filename The desired name of the file in Braintrust after uploading.
1042
+ * This parameter is for visualization purposes only and has no effect on
1043
+ * attachment storage.
1044
+ *
1045
+ * @param contentType The MIME type of the file.
1046
+ *
1047
+ * @param state (Optional) For internal use.
1048
+ */
1049
+ constructor({ data, filename, contentType, state }) {
1050
+ this.reference = {
1051
+ type: import_typespecs2.BRAINTRUST_ATTACHMENT,
1052
+ filename,
1053
+ content_type: contentType,
1054
+ key: newId()
1055
+ };
1056
+ this.state = state;
1057
+ this.dataDebugString = typeof data === "string" ? data : "<in-memory data>";
1058
+ this.data = this.initData(data);
1059
+ this.uploader = this.initUploader();
1060
+ }
1061
+ /**
1062
+ * On first access, (1) reads the attachment from disk if needed, (2)
1063
+ * authenticates with the data plane to request a signed URL, (3) uploads to
1064
+ * object store, and (4) updates the attachment.
1065
+ *
1066
+ * @returns The attachment status.
1067
+ */
1068
+ async upload() {
1069
+ return await this.uploader.get();
1070
+ }
1071
+ /**
1072
+ * A human-readable description for logging and debugging.
1073
+ *
1074
+ * @returns The debug object. The return type is not stable and may change in
1075
+ * a future release.
1076
+ */
1077
+ debugInfo() {
1078
+ return {
1079
+ inputData: this.dataDebugString,
1080
+ reference: this.reference,
1081
+ state: this.state
1082
+ };
1083
+ }
1084
+ initUploader() {
1085
+ const doUpload = async (conn, orgId) => {
1086
+ const requestParams = {
1087
+ key: this.reference.key,
1088
+ filename: this.reference.filename,
1089
+ content_type: this.reference.content_type,
1090
+ org_id: orgId
1091
+ };
1092
+ const [metadataPromiseResult, dataPromiseResult] = await Promise.allSettled([
1093
+ conn.post("/attachment", requestParams),
1094
+ this.data.get()
1095
+ ]);
1096
+ if (metadataPromiseResult.status === "rejected") {
1097
+ const errorStr = JSON.stringify(metadataPromiseResult.reason);
1098
+ throw new Error(
1099
+ `Failed to request signed URL from API server: ${errorStr}`
1100
+ );
1101
+ }
1102
+ if (dataPromiseResult.status === "rejected") {
1103
+ const errorStr = JSON.stringify(dataPromiseResult.reason);
1104
+ throw new Error(`Failed to read file: ${errorStr}`);
1105
+ }
1106
+ const metadataResponse = metadataPromiseResult.value;
1107
+ const data = dataPromiseResult.value;
1108
+ let signedUrl;
1109
+ let headers;
1110
+ try {
1111
+ ({ signedUrl, headers } = import_zod2.z.object({
1112
+ signedUrl: import_zod2.z.string().url(),
1113
+ headers: import_zod2.z.record(import_zod2.z.string())
1114
+ }).parse(await metadataResponse.json()));
1115
+ } catch (error2) {
1116
+ if (error2 instanceof import_zod2.ZodError) {
1117
+ const errorStr = JSON.stringify(error2.flatten());
1118
+ throw new Error(`Invalid response from API server: ${errorStr}`);
1119
+ }
1120
+ throw error2;
1121
+ }
1122
+ let objectStoreResponse;
1123
+ try {
1124
+ objectStoreResponse = await checkResponse(
1125
+ await fetch(signedUrl, {
1126
+ method: "PUT",
1127
+ headers,
1128
+ body: data
1129
+ })
1130
+ );
1131
+ } catch (error2) {
1132
+ if (error2 instanceof FailedHTTPResponse) {
1133
+ throw new Error(
1134
+ `Failed to upload attachment to object store: ${error2.status} ${error2.text} ${error2.data}`
1135
+ );
1136
+ }
1137
+ throw error2;
1138
+ }
1139
+ return { signedUrl, metadataResponse, objectStoreResponse };
1140
+ };
1141
+ const errorWrapper = async () => {
1142
+ const status = { upload_status: "done" };
1143
+ const state = this.state ?? _globalState;
1144
+ await state.login({});
1145
+ const conn = state.apiConn();
1146
+ const orgId = state.orgId ?? "";
1147
+ try {
1148
+ await doUpload(conn, orgId);
1149
+ } catch (error2) {
1150
+ status.upload_status = "error";
1151
+ status.error_message = error2 instanceof Error ? error2.message : JSON.stringify(error2);
1152
+ }
1153
+ const requestParams = {
1154
+ key: this.reference.key,
1155
+ org_id: orgId,
1156
+ status
1157
+ };
1158
+ const statusResponse = await conn.post(
1159
+ "/attachment/status",
1160
+ requestParams
1161
+ );
1162
+ if (!statusResponse.ok) {
1163
+ const errorStr = JSON.stringify(statusResponse);
1164
+ throw new Error(`Couldn't log attachment status: ${errorStr}`);
1165
+ }
1166
+ return status;
1167
+ };
1168
+ return new LazyValue(errorWrapper);
1169
+ }
1170
+ initData(data) {
1171
+ if (typeof data === "string") {
1172
+ const readFile2 = isomorph_default.readFile;
1173
+ if (!readFile2) {
1174
+ throw new Error(
1175
+ `This platform does not support reading the filesystem. Construct the Attachment
1176
+ with a Blob/ArrayBuffer, or run the program on Node.js.`
1177
+ );
1178
+ }
1179
+ return new LazyValue(async () => new Blob([await readFile2(data)]));
1180
+ } else {
1181
+ return new LazyValue(async () => new Blob([data]));
1182
+ }
1183
+ }
1184
+ };
1014
1185
  function logFeedbackImpl(state, parentObjectType, parentObjectId, {
1015
1186
  id,
1016
1187
  expected,
@@ -1035,7 +1206,7 @@ function logFeedbackImpl(state, parentObjectType, parentObjectId, {
1035
1206
  expected,
1036
1207
  tags
1037
1208
  });
1038
- let { metadata, ...updateEvent } = validatedEvent;
1209
+ let { metadata, ...updateEvent } = deepCopyEvent(validatedEvent);
1039
1210
  updateEvent = Object.fromEntries(
1040
1211
  Object.entries(updateEvent).filter(([_, v]) => !isEmpty(v))
1041
1212
  );
@@ -1084,10 +1255,12 @@ function updateSpanImpl({
1084
1255
  id,
1085
1256
  event
1086
1257
  }) {
1087
- const updateEvent = validateAndSanitizeExperimentLogPartialArgs({
1088
- id,
1089
- ...event
1090
- });
1258
+ const updateEvent = deepCopyEvent(
1259
+ validateAndSanitizeExperimentLogPartialArgs({
1260
+ id,
1261
+ ...event
1262
+ })
1263
+ );
1091
1264
  const parentIds = async () => new import_core.SpanComponentsV3({
1092
1265
  object_type: parentObjectType,
1093
1266
  object_id: await parentObjectId.get()
@@ -1288,7 +1461,7 @@ var Logger = class {
1288
1461
  * @param event.id: (Optional) a unique identifier for the event. If you don't provide one, BrainTrust will generate one for you.
1289
1462
  * @param options Additional logging options
1290
1463
  * @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.
1291
- * :returns: The `id` of the logged event.
1464
+ * @returns The `id` of the logged event.
1292
1465
  */
1293
1466
  log(event, options) {
1294
1467
  if (this.calledStartSpan && !options?.allowConcurrentWithSpans) {
@@ -1311,7 +1484,7 @@ var Logger = class {
1311
1484
  /**
1312
1485
  * Create a new toplevel span underneath the logger. The name defaults to "root".
1313
1486
  *
1314
- * See `Span.traced` for full details.
1487
+ * See {@link Span.traced} for full details.
1315
1488
  */
1316
1489
  traced(callback, args) {
1317
1490
  const { setCurrent, ...argsRest } = args ?? {};
@@ -1345,7 +1518,7 @@ var Logger = class {
1345
1518
  * where you cannot use callbacks. However, spans started with `startSpan` will not be marked as the "current span",
1346
1519
  * so `currentSpan()` and `traced()` will be no-ops. If you want to mark a span as current, use `traced` instead.
1347
1520
  *
1348
- * See `traced` for full details.
1521
+ * See {@link traced} for full details.
1349
1522
  */
1350
1523
  startSpan(args) {
1351
1524
  this.calledStartSpan = true;
@@ -1385,7 +1558,7 @@ var Logger = class {
1385
1558
  * 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,
1386
1559
  * since otherwise updates to the span may conflict with the original span.
1387
1560
  *
1388
- * @param event The event data to update the span with. Must include `id`. See `Experiment.log` for a full list of valid fields.
1561
+ * @param event The event data to update the span with. Must include `id`. See {@link Experiment.log} for a full list of valid fields.
1389
1562
  */
1390
1563
  updateSpan(event) {
1391
1564
  const { id, ...eventRest } = event;
@@ -1401,7 +1574,9 @@ var Logger = class {
1401
1574
  });
1402
1575
  }
1403
1576
  /**
1404
- * Return a serialized representation of the logger that can be used to start subspans in other places. See `Span.start_span` for more details.
1577
+ * Return a serialized representation of the logger that can be used to start subspans in other places.
1578
+ *
1579
+ * See {@link Span.startSpan} for more details.
1405
1580
  */
1406
1581
  async export() {
1407
1582
  return new import_core.SpanComponentsV3({
@@ -1441,6 +1616,7 @@ var BackgroundLogger = class _BackgroundLogger {
1441
1616
  activeFlush = Promise.resolve();
1442
1617
  activeFlushResolved = true;
1443
1618
  activeFlushError = void 0;
1619
+ onFlushError;
1444
1620
  syncFlush = false;
1445
1621
  // 6 MB for the AWS lambda gateway (from our own testing).
1446
1622
  maxRequestSize = 6 * 1024 * 1024;
@@ -1504,6 +1680,7 @@ var BackgroundLogger = class _BackgroundLogger {
1504
1680
  await this.flush();
1505
1681
  });
1506
1682
  }
1683
+ this.onFlushError = opts.onFlushError;
1507
1684
  }
1508
1685
  log(items) {
1509
1686
  const [addedItems, droppedItems] = (() => {
@@ -1535,14 +1712,16 @@ var BackgroundLogger = class _BackgroundLogger {
1535
1712
  if (this.activeFlushError) {
1536
1713
  const err = this.activeFlushError;
1537
1714
  this.activeFlushError = void 0;
1538
- throw err;
1715
+ if (this.syncFlush) {
1716
+ throw err;
1717
+ }
1539
1718
  }
1540
1719
  }
1541
1720
  async flushOnce(args) {
1542
1721
  const batchSize = args?.batchSize ?? this.defaultBatchSize;
1543
1722
  const wrappedItems = this.items;
1544
1723
  this.items = [];
1545
- const allItems = await this.unwrapLazyValues(wrappedItems);
1724
+ const [allItems, attachments] = await this.unwrapLazyValues(wrappedItems);
1546
1725
  if (allItems.length === 0) {
1547
1726
  return;
1548
1727
  }
@@ -1574,6 +1753,23 @@ var BackgroundLogger = class _BackgroundLogger {
1574
1753
  );
1575
1754
  }
1576
1755
  }
1756
+ const attachmentErrors = [];
1757
+ for (const attachment of attachments) {
1758
+ try {
1759
+ const result = await attachment.upload();
1760
+ if (result.upload_status === "error" && result.error_message) {
1761
+ attachmentErrors.push(new Error(result.error_message));
1762
+ }
1763
+ } catch (error2) {
1764
+ attachmentErrors.push(error2);
1765
+ }
1766
+ }
1767
+ if (attachmentErrors.length > 0) {
1768
+ throw new AggregateError(
1769
+ attachmentErrors,
1770
+ `Encountered the following errors while uploading attachments:`
1771
+ );
1772
+ }
1577
1773
  if (this.items.length > 0) {
1578
1774
  await this.flushOnce(args);
1579
1775
  }
@@ -1581,8 +1777,10 @@ var BackgroundLogger = class _BackgroundLogger {
1581
1777
  async unwrapLazyValues(wrappedItems) {
1582
1778
  for (let i = 0; i < this.numTries; ++i) {
1583
1779
  try {
1584
- const itemPromises = wrappedItems.map((x) => x.get());
1585
- return (0, import_core.mergeRowBatch)(await Promise.all(itemPromises));
1780
+ const items = await Promise.all(wrappedItems.map((x) => x.get()));
1781
+ const attachments = [];
1782
+ items.forEach((item) => extractAttachments(item, attachments));
1783
+ return [(0, import_core.mergeRowBatch)(items), attachments];
1586
1784
  } catch (e) {
1587
1785
  let errmsg = "Encountered error when constructing records to flush";
1588
1786
  const isRetrying = i + 1 < this.numTries;
@@ -1590,7 +1788,10 @@ var BackgroundLogger = class _BackgroundLogger {
1590
1788
  errmsg += ". Retrying";
1591
1789
  }
1592
1790
  console.warn(errmsg);
1593
- if (!isRetrying && this.syncFlush) {
1791
+ if (!isRetrying) {
1792
+ console.warn(
1793
+ `Failed to construct log records to flush after ${this.numTries} attempts. Dropping batch`
1794
+ );
1594
1795
  throw e;
1595
1796
  } else {
1596
1797
  console.warn(e);
@@ -1598,10 +1799,7 @@ var BackgroundLogger = class _BackgroundLogger {
1598
1799
  }
1599
1800
  }
1600
1801
  }
1601
- console.warn(
1602
- `Failed to construct log records to flush after ${this.numTries} attempts. Dropping batch`
1603
- );
1604
- return [];
1802
+ throw new Error("Impossible");
1605
1803
  }
1606
1804
  async submitLogsRequest(items) {
1607
1805
  const conn = await this.apiConn.get();
@@ -1617,16 +1815,14 @@ var BackgroundLogger = class _BackgroundLogger {
1617
1815
  let error2 = void 0;
1618
1816
  try {
1619
1817
  await conn.post_json("logs3", dataStr);
1620
- } catch (e) {
1818
+ } catch {
1621
1819
  try {
1622
1820
  const legacyDataS = (0, import_core.constructJsonArray)(
1623
- items.map(
1624
- (r) => JSON.stringify((0, import_core.makeLegacyEvent)(JSON.parse(r)))
1625
- )
1821
+ items.map((r) => JSON.stringify((0, import_core.makeLegacyEvent)(JSON.parse(r))))
1626
1822
  );
1627
1823
  await conn.post_json("logs", legacyDataS);
1628
- } catch (e2) {
1629
- error2 = e2;
1824
+ } catch (e) {
1825
+ error2 = e;
1630
1826
  }
1631
1827
  }
1632
1828
  if (error2 === void 0) {
@@ -1650,7 +1846,10 @@ Error: ${errorText}`;
1650
1846
  });
1651
1847
  this.logFailedPayloadsDir();
1652
1848
  }
1653
- if (!isRetrying && this.syncFlush) {
1849
+ if (!isRetrying) {
1850
+ console.warn(
1851
+ `log request failed after ${this.numTries} retries. Dropping batch`
1852
+ );
1654
1853
  throw new Error(errMsg);
1655
1854
  } else {
1656
1855
  console.warn(errMsg);
@@ -1659,10 +1858,6 @@ Error: ${errorText}`;
1659
1858
  }
1660
1859
  }
1661
1860
  }
1662
- console.warn(
1663
- `log request failed after ${this.numTries} retries. Dropping batch`
1664
- );
1665
- return;
1666
1861
  }
1667
1862
  registerDroppedItemCount(numItems) {
1668
1863
  if (numItems <= 0) {
@@ -1690,15 +1885,17 @@ Error: ${errorText}`;
1690
1885
  return;
1691
1886
  }
1692
1887
  try {
1693
- const allItems = await this.unwrapLazyValues(wrappedItems);
1888
+ const [allItems, allAttachments] = await this.unwrapLazyValues(wrappedItems);
1694
1889
  const dataStr = constructLogs3Data(
1695
1890
  allItems.map((x) => JSON.stringify(x))
1696
1891
  );
1892
+ const attachmentStr = JSON.stringify(
1893
+ allAttachments.map((a) => a.debugInfo())
1894
+ );
1895
+ const payload = `{"data": ${dataStr}, "attachments": ${attachmentStr}}
1896
+ `;
1697
1897
  for (const payloadDir of publishPayloadsDir) {
1698
- await _BackgroundLogger.writePayloadToDir({
1699
- payloadDir,
1700
- payload: dataStr
1701
- });
1898
+ await _BackgroundLogger.writePayloadToDir({ payloadDir, payload });
1702
1899
  }
1703
1900
  } catch (e) {
1704
1901
  console.error(e);
@@ -1737,6 +1934,13 @@ Error: ${errorText}`;
1737
1934
  try {
1738
1935
  await this.flushOnce();
1739
1936
  } catch (err) {
1937
+ if (err instanceof AggregateError) {
1938
+ for (const e of err.errors) {
1939
+ this.onFlushError?.(e);
1940
+ }
1941
+ } else {
1942
+ this.onFlushError?.(err);
1943
+ }
1740
1944
  this.activeFlushError = err;
1741
1945
  } finally {
1742
1946
  this.activeFlushResolved = true;
@@ -1779,7 +1983,7 @@ function init(projectOrOptions, optionalOptions) {
1779
1983
  apiKey,
1780
1984
  orgName,
1781
1985
  forceLogin,
1782
- fetch,
1986
+ fetch: fetch2,
1783
1987
  metadata,
1784
1988
  gitMetadataSettings,
1785
1989
  projectId,
@@ -1797,7 +2001,7 @@ function init(projectOrOptions, optionalOptions) {
1797
2001
  }
1798
2002
  const lazyMetadata2 = new LazyValue(
1799
2003
  async () => {
1800
- await state.login({ apiKey, appUrl, orgName, fetch, forceLogin });
2004
+ await state.login({ apiKey, appUrl, orgName, fetch: fetch2, forceLogin });
1801
2005
  const args = {
1802
2006
  project_name: project,
1803
2007
  project_id: projectId,
@@ -1968,7 +2172,7 @@ function initDataset(projectOrOptions, optionalOptions) {
1968
2172
  appUrl,
1969
2173
  apiKey,
1970
2174
  orgName,
1971
- fetch,
2175
+ fetch: fetch2,
1972
2176
  forceLogin,
1973
2177
  projectId,
1974
2178
  metadata,
@@ -1982,7 +2186,7 @@ function initDataset(projectOrOptions, optionalOptions) {
1982
2186
  orgName,
1983
2187
  apiKey,
1984
2188
  appUrl,
1985
- fetch,
2189
+ fetch: fetch2,
1986
2190
  forceLogin
1987
2191
  });
1988
2192
  const args = {
@@ -2064,7 +2268,7 @@ function initLogger(options = {}) {
2064
2268
  apiKey,
2065
2269
  orgName,
2066
2270
  forceLogin,
2067
- fetch,
2271
+ fetch: fetch2,
2068
2272
  state: stateArg
2069
2273
  } = options || {};
2070
2274
  const computeMetadataArgs = {
@@ -2079,7 +2283,7 @@ function initLogger(options = {}) {
2079
2283
  apiKey,
2080
2284
  appUrl,
2081
2285
  forceLogin,
2082
- fetch
2286
+ fetch: fetch2
2083
2287
  });
2084
2288
  return computeLoggerMetadata(state, computeMetadataArgs);
2085
2289
  }
@@ -2103,7 +2307,7 @@ async function loadPrompt({
2103
2307
  appUrl,
2104
2308
  apiKey,
2105
2309
  orgName,
2106
- fetch,
2310
+ fetch: fetch2,
2107
2311
  forceLogin,
2108
2312
  state: stateArg
2109
2313
  }) {
@@ -2118,7 +2322,7 @@ async function loadPrompt({
2118
2322
  orgName,
2119
2323
  apiKey,
2120
2324
  appUrl,
2121
- fetch,
2325
+ fetch: fetch2,
2122
2326
  forceLogin
2123
2327
  });
2124
2328
  const args = {
@@ -2169,7 +2373,7 @@ async function loginToState(options = {}) {
2169
2373
  appUrl = isomorph_default.getEnv("BRAINTRUST_APP_URL") || "https://www.braintrust.dev",
2170
2374
  apiKey = isomorph_default.getEnv("BRAINTRUST_API_KEY"),
2171
2375
  orgName = isomorph_default.getEnv("BRAINTRUST_ORG_NAME"),
2172
- fetch = globalThis.fetch
2376
+ fetch: fetch2 = globalThis.fetch
2173
2377
  } = options || {};
2174
2378
  const appPublicUrl = isomorph_default.getEnv("BRAINTRUST_APP_PUBLIC_URL") || appUrl;
2175
2379
  const state = new BraintrustState(options);
@@ -2179,7 +2383,7 @@ async function loginToState(options = {}) {
2179
2383
  let conn = null;
2180
2384
  if (apiKey !== void 0) {
2181
2385
  const resp = await checkResponse(
2182
- await fetch((0, import_core._urljoin)(state.appUrl, `/api/apikey/login`), {
2386
+ await fetch2((0, import_core._urljoin)(state.appUrl, `/api/apikey/login`), {
2183
2387
  method: "POST",
2184
2388
  headers: {
2185
2389
  "Content-Type": "application/json",
@@ -2347,8 +2551,8 @@ async function flush(options) {
2347
2551
  const state = options?.state ?? _globalState;
2348
2552
  return await state.bgLogger().flush();
2349
2553
  }
2350
- function setFetch(fetch) {
2351
- _globalState.setFetch(fetch);
2554
+ function setFetch(fetch2) {
2555
+ _globalState.setFetch(fetch2);
2352
2556
  }
2353
2557
  function startSpanAndIsLogger(args) {
2354
2558
  const state = args?.state ?? _globalState;
@@ -2476,6 +2680,47 @@ function validateAndSanitizeExperimentLogPartialArgs(event) {
2476
2680
  return { ...event };
2477
2681
  }
2478
2682
  }
2683
+ function deepCopyEvent(event) {
2684
+ const attachments = [];
2685
+ const IDENTIFIER = "_bt_internal_saved_attachment";
2686
+ const savedAttachmentSchema = import_zod2.z.strictObject({ [IDENTIFIER]: import_zod2.z.number() });
2687
+ const serialized = JSON.stringify(event, (_k, v) => {
2688
+ if (v instanceof SpanImpl || v instanceof NoopSpan) {
2689
+ return `<span>`;
2690
+ } else if (v instanceof Experiment) {
2691
+ return `<experiment>`;
2692
+ } else if (v instanceof Dataset) {
2693
+ return `<dataset>`;
2694
+ } else if (v instanceof Logger) {
2695
+ return `<logger>`;
2696
+ } else if (v instanceof Attachment) {
2697
+ const idx = attachments.push(v);
2698
+ return { [IDENTIFIER]: idx - 1 };
2699
+ }
2700
+ return v;
2701
+ });
2702
+ const x = JSON.parse(serialized, (_k, v) => {
2703
+ const parsedAttachment = savedAttachmentSchema.safeParse(v);
2704
+ if (parsedAttachment.success) {
2705
+ return attachments[parsedAttachment.data[IDENTIFIER]];
2706
+ }
2707
+ return v;
2708
+ });
2709
+ return x;
2710
+ }
2711
+ function extractAttachments(event, attachments) {
2712
+ for (const [key, value] of Object.entries(event)) {
2713
+ if (value instanceof Attachment) {
2714
+ attachments.push(value);
2715
+ event[key] = value.reference;
2716
+ continue;
2717
+ }
2718
+ if (!(value instanceof Object)) {
2719
+ continue;
2720
+ }
2721
+ extractAttachments(value, attachments);
2722
+ }
2723
+ }
2479
2724
  function validateAndSanitizeExperimentLogFullArgs(event, hasDataset) {
2480
2725
  if ("input" in event && !isEmpty(event.input) && "inputs" in event && !isEmpty(event.inputs) || !("input" in event) && !("inputs" in event)) {
2481
2726
  throw new Error(
@@ -2606,10 +2851,9 @@ var Experiment = class extends ObjectFetcher {
2606
2851
  * @param event.metrics: (Optional) a dictionary of metrics to log. The following keys are populated automatically: "start", "end".
2607
2852
  * @param event.id: (Optional) a unique identifier for the event. If you don't provide one, BrainTrust will generate one for you.
2608
2853
  * @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.
2609
- * @param event.inputs: (Deprecated) the same as `input` (will be removed in a future version).
2610
2854
  * @param options Additional logging options
2611
2855
  * @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.
2612
- * :returns: The `id` of the logged event.
2856
+ * @returns The `id` of the logged event.
2613
2857
  */
2614
2858
  log(event, options) {
2615
2859
  if (this.calledStartSpan && !options?.allowConcurrentWithSpans) {
@@ -2625,7 +2869,7 @@ var Experiment = class extends ObjectFetcher {
2625
2869
  /**
2626
2870
  * Create a new toplevel span underneath the experiment. The name defaults to "root".
2627
2871
  *
2628
- * See `Span.traced` for full details.
2872
+ * See {@link Span.traced} for full details.
2629
2873
  */
2630
2874
  traced(callback, args) {
2631
2875
  const { setCurrent, ...argsRest } = args ?? {};
@@ -2651,7 +2895,7 @@ var Experiment = class extends ObjectFetcher {
2651
2895
  * where you cannot use callbacks. However, spans started with `startSpan` will not be marked as the "current span",
2652
2896
  * so `currentSpan()` and `traced()` will be no-ops. If you want to mark a span as current, use `traced` instead.
2653
2897
  *
2654
- * See `traced` for full details.
2898
+ * See {@link traced} for full details.
2655
2899
  */
2656
2900
  startSpan(args) {
2657
2901
  this.calledStartSpan = true;
@@ -2763,7 +3007,7 @@ var Experiment = class extends ObjectFetcher {
2763
3007
  * 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,
2764
3008
  * since otherwise updates to the span may conflict with the original span.
2765
3009
  *
2766
- * @param event The event data to update the span with. Must include `id`. See `Experiment.log` for a full list of valid fields.
3010
+ * @param event The event data to update the span with. Must include `id`. See {@link Experiment.log} for a full list of valid fields.
2767
3011
  */
2768
3012
  updateSpan(event) {
2769
3013
  const { id, ...eventRest } = event;
@@ -2779,7 +3023,9 @@ var Experiment = class extends ObjectFetcher {
2779
3023
  });
2780
3024
  }
2781
3025
  /**
2782
- * Return a serialized representation of the experiment that can be used to start subspans in other places. See `Span.start_span` for more details.
3026
+ * Return a serialized representation of the experiment that can be used to start subspans in other places.
3027
+ *
3028
+ * See {@link Span.startSpan} for more details.
2783
3029
  */
2784
3030
  async export() {
2785
3031
  return new import_core.SpanComponentsV3({
@@ -2794,7 +3040,7 @@ var Experiment = class extends ObjectFetcher {
2794
3040
  return await this.state.bgLogger().flush();
2795
3041
  }
2796
3042
  /**
2797
- * This function is deprecated. You can simply remove it from your code.
3043
+ * @deprecated This function is deprecated. You can simply remove it from your code.
2798
3044
  */
2799
3045
  async close() {
2800
3046
  console.warn(
@@ -2936,27 +3182,14 @@ var SpanImpl = class _SpanImpl {
2936
3182
  event,
2937
3183
  internalData
2938
3184
  });
2939
- let partialRecord = {
3185
+ const partialRecord = deepCopyEvent({
2940
3186
  id: this.id,
2941
3187
  span_id: this.spanId,
2942
3188
  root_span_id: this.rootSpanId,
2943
3189
  span_parents: this.spanParents,
2944
3190
  ...serializableInternalData,
2945
3191
  [import_core.IS_MERGE_FIELD]: this.isMerge
2946
- };
2947
- const serializedPartialRecord = JSON.stringify(partialRecord, (_k, v) => {
2948
- if (v instanceof _SpanImpl) {
2949
- return `<span>`;
2950
- } else if (v instanceof Experiment) {
2951
- return `<experiment>`;
2952
- } else if (v instanceof Dataset) {
2953
- return `<dataset>`;
2954
- } else if (v instanceof Logger) {
2955
- return `<logger>`;
2956
- }
2957
- return v;
2958
3192
  });
2959
- partialRecord = JSON.parse(serializedPartialRecord);
2960
3193
  if (partialRecord.metrics?.end) {
2961
3194
  this.loggedEndTime = partialRecord.metrics?.end;
2962
3195
  }
@@ -3042,6 +3275,11 @@ var SpanImpl = class _SpanImpl {
3042
3275
  propagated_event: this.propagatedEvent
3043
3276
  }).toStr();
3044
3277
  }
3278
+ async permalink() {
3279
+ return await permalink(await this.export(), {
3280
+ state: this.state
3281
+ });
3282
+ }
3045
3283
  async flush() {
3046
3284
  return await this.state.bgLogger().flush();
3047
3285
  }
@@ -3191,15 +3429,17 @@ var Dataset = class extends ObjectFetcher {
3191
3429
  }) {
3192
3430
  this.validateEvent({ metadata, expected, output, tags });
3193
3431
  const rowId = id || (0, import_uuid.v4)();
3194
- const args = this.createArgs({
3195
- id: rowId,
3196
- input,
3197
- expected,
3198
- metadata,
3199
- tags,
3200
- output,
3201
- isMerge: false
3202
- });
3432
+ const args = this.createArgs(
3433
+ deepCopyEvent({
3434
+ id: rowId,
3435
+ input,
3436
+ expected,
3437
+ metadata,
3438
+ tags,
3439
+ output,
3440
+ isMerge: false
3441
+ })
3442
+ );
3203
3443
  this.state.bgLogger().log([args]);
3204
3444
  return rowId;
3205
3445
  }
@@ -3224,14 +3464,16 @@ var Dataset = class extends ObjectFetcher {
3224
3464
  id
3225
3465
  }) {
3226
3466
  this.validateEvent({ metadata, expected, tags });
3227
- const args = this.createArgs({
3228
- id,
3229
- input,
3230
- expected,
3231
- metadata,
3232
- tags,
3233
- isMerge: true
3234
- });
3467
+ const args = this.createArgs(
3468
+ deepCopyEvent({
3469
+ id,
3470
+ input,
3471
+ expected,
3472
+ metadata,
3473
+ tags,
3474
+ isMerge: true
3475
+ })
3476
+ );
3235
3477
  this.state.bgLogger().log([args]);
3236
3478
  return id;
3237
3479
  }
@@ -3286,7 +3528,7 @@ var Dataset = class extends ObjectFetcher {
3286
3528
  return await this.state.bgLogger().flush();
3287
3529
  }
3288
3530
  /**
3289
- * This function is deprecated. You can simply remove it from your code.
3531
+ * @deprecated This function is deprecated. You can simply remove it from your code.
3290
3532
  */
3291
3533
  async close() {
3292
3534
  console.warn(
@@ -3450,6 +3692,7 @@ var Prompt = class {
3450
3692
  return this.parsedPromptData;
3451
3693
  }
3452
3694
  };
3695
+ var _exportsForTestingOnly = { extractAttachments, deepCopyEvent };
3453
3696
 
3454
3697
  // src/node.ts
3455
3698
  function configureNode() {
@@ -3465,12 +3708,14 @@ function configureNode() {
3465
3708
  isomorph_default.pathDirname = path.dirname;
3466
3709
  isomorph_default.mkdir = fs.mkdir;
3467
3710
  isomorph_default.writeFile = fs.writeFile;
3711
+ isomorph_default.readFile = fs.readFile;
3468
3712
  _internalSetInitialState();
3469
3713
  }
3470
3714
 
3471
3715
  // src/exports-node.ts
3472
3716
  var exports_node_exports = {};
3473
3717
  __export(exports_node_exports, {
3718
+ Attachment: () => Attachment,
3474
3719
  BaseExperiment: () => BaseExperiment,
3475
3720
  BraintrustState: () => BraintrustState,
3476
3721
  BraintrustStream: () => BraintrustStream,
@@ -3493,6 +3738,7 @@ __export(exports_node_exports, {
3493
3738
  SpanImpl: () => SpanImpl,
3494
3739
  ToolBuilder: () => ToolBuilder,
3495
3740
  X_CACHED_HEADER: () => X_CACHED_HEADER,
3741
+ _exportsForTestingOnly: () => _exportsForTestingOnly,
3496
3742
  _internalGetGlobalState: () => _internalGetGlobalState,
3497
3743
  _internalSetInitialState: () => _internalSetInitialState,
3498
3744
  braintrustStreamChunkSchema: () => braintrustStreamChunkSchema,
@@ -3546,7 +3792,7 @@ async function invoke(args) {
3546
3792
  apiKey,
3547
3793
  appUrl,
3548
3794
  forceLogin,
3549
- fetch,
3795
+ fetch: fetch2,
3550
3796
  input,
3551
3797
  messages,
3552
3798
  parent: parentArg,
@@ -3562,7 +3808,7 @@ async function invoke(args) {
3562
3808
  apiKey,
3563
3809
  appUrl,
3564
3810
  forceLogin,
3565
- fetch
3811
+ fetch: fetch2
3566
3812
  });
3567
3813
  const parent = parentArg ? typeof parentArg === "string" ? parentArg : await parentArg.export() : await getSpanParentObject().export();
3568
3814
  const functionId = import_typespecs3.functionIdSchema.safeParse({
@@ -4813,6 +5059,7 @@ async function Eval(name, evaluator, reporterOrOpts) {
4813
5059
  }
4814
5060
  if (globalThis._lazy_load) {
4815
5061
  globalThis._evals.evaluators[evalName] = {
5062
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
4816
5063
  evaluator: {
4817
5064
  evalName,
4818
5065
  projectName: name,
@@ -4851,7 +5098,8 @@ async function Eval(name, evaluator, reporterOrOpts) {
4851
5098
  baseExperiment: evaluator.baseExperimentName ?? defaultBaseExperiment,
4852
5099
  baseExperimentId: evaluator.baseExperimentId,
4853
5100
  gitMetadataSettings: evaluator.gitMetadataSettings,
4854
- repoInfo: evaluator.repoInfo
5101
+ repoInfo: evaluator.repoInfo,
5102
+ dataset: data instanceof Dataset ? data : void 0
4855
5103
  });
4856
5104
  if (options.onStart) {
4857
5105
  experiment.summarize({ summarizeScores: false }).then(options.onStart);
@@ -4897,7 +5145,10 @@ function serializeJSONWithPlainString(v) {
4897
5145
  function evaluateFilter(object, filter2) {
4898
5146
  const { path: path3, pattern } = filter2;
4899
5147
  const key = path3.reduce(
4900
- (acc, p) => typeof acc === "object" && acc !== null ? acc[p] : void 0,
5148
+ (acc, p) => typeof acc === "object" && acc !== null ? (
5149
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
5150
+ acc[p]
5151
+ ) : void 0,
4901
5152
  object
4902
5153
  );
4903
5154
  if (key === void 0) {
@@ -5004,7 +5255,12 @@ async function runEvaluatorInternal(experiment, evaluator, progressReporter, fil
5004
5255
  }
5005
5256
  );
5006
5257
  rootSpan.log({ output, metadata });
5007
- const scoringArgs = { ...datum, metadata, output };
5258
+ const scoringArgs = {
5259
+ input: datum.input,
5260
+ expected: "expected" in datum ? datum.expected : void 0,
5261
+ metadata,
5262
+ output
5263
+ };
5008
5264
  const scorerNames = evaluator.scores.map(scorerName);
5009
5265
  const scoreResults = await Promise.all(
5010
5266
  evaluator.scores.map(async (score, score_idx) => {
@@ -5131,7 +5387,13 @@ async function runEvaluatorInternal(experiment, evaluator, progressReporter, fil
5131
5387
  event: {
5132
5388
  input: datum.input,
5133
5389
  expected: "expected" in datum ? datum.expected : void 0,
5134
- tags: datum.tags
5390
+ tags: datum.tags,
5391
+ origin: experiment.dataset && datum.id && datum._xact_id ? {
5392
+ object_type: "dataset",
5393
+ object_id: await experiment.dataset.id,
5394
+ id: datum.id,
5395
+ _xact_id: datum._xact_id
5396
+ } : void 0
5135
5397
  }
5136
5398
  });
5137
5399
  }
@@ -5994,6 +6256,7 @@ configureNode();
5994
6256
  var src_default = exports_node_exports;
5995
6257
  // Annotate the CommonJS export names for ESM import in node:
5996
6258
  0 && (module.exports = {
6259
+ Attachment,
5997
6260
  BaseExperiment,
5998
6261
  BraintrustState,
5999
6262
  BraintrustStream,
@@ -6016,6 +6279,7 @@ var src_default = exports_node_exports;
6016
6279
  SpanImpl,
6017
6280
  ToolBuilder,
6018
6281
  X_CACHED_HEADER,
6282
+ _exportsForTestingOnly,
6019
6283
  _internalGetGlobalState,
6020
6284
  _internalSetInitialState,
6021
6285
  braintrustStreamChunkSchema,