@flue/sdk 0.4.1 → 0.5.1

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.
@@ -1,10 +1,33 @@
1
1
  import { a as buildSkillByPathPrompt, c as createTools, f as resolveSkillFilePath, i as buildSkillByNamePrompt, l as formatBashResult, n as buildPromptText, o as createResultTools, p as skillsDirIn, r as buildResultFollowUpPrompt, s as BUILTIN_TOOL_NAMES, t as ResultUnavailableError } from "./result-K1IRhWKM.mjs";
2
- import { i as getRegisteredApiKey, r as getProviderConfiguration } from "./providers-DeFRIwp0.mjs";
2
+ import { i as getRegisteredApiKey, l as generateOperationId, r as getProviderConfiguration } from "./providers-BjEEoKLy.mjs";
3
3
  import { n as createCallHandle, t as abortErrorFor } from "./abort-Bg3qsAkU.mjs";
4
4
  import { createFlueFs } from "./sandbox.mjs";
5
5
  import { completeSimple, isContextOverflow } from "@mariozechner/pi-ai";
6
6
  import { Agent } from "@mariozechner/pi-agent-core";
7
7
 
8
+ //#region src/roles.ts
9
+ function assertRoleExists(roles, roleName) {
10
+ if (!roleName) return;
11
+ if (roles[roleName]) return;
12
+ const available = Object.keys(roles);
13
+ const list = available.length > 0 ? available.join(", ") : "(none defined)";
14
+ throw new Error(`[flue] Role "${roleName}" not registered. Available roles: ${list}. Define roles as markdown files in \`roles/\` (or \`.flue/roles/\` if your root uses the .flue/ source layout).`);
15
+ }
16
+ function resolveEffectiveRole(options) {
17
+ const role = options.callRole ?? options.sessionRole ?? options.agentRole;
18
+ assertRoleExists(options.roles, role);
19
+ return role;
20
+ }
21
+ function resolveRoleModel(roles, roleName) {
22
+ assertRoleExists(roles, roleName);
23
+ return roleName ? roles[roleName]?.model : void 0;
24
+ }
25
+ function resolveRoleThinkingLevel(roles, roleName) {
26
+ assertRoleExists(roles, roleName);
27
+ return roleName ? roles[roleName]?.thinkingLevel : void 0;
28
+ }
29
+
30
+ //#endregion
8
31
  //#region src/usage.ts
9
32
  /** All-zero `PromptUsage`. Identity element for `addUsage`. */
10
33
  function emptyUsage() {
@@ -473,29 +496,6 @@ async function compact(preparation, model, apiKey, signal) {
473
496
  };
474
497
  }
475
498
 
476
- //#endregion
477
- //#region src/roles.ts
478
- function assertRoleExists(roles, roleName) {
479
- if (!roleName) return;
480
- if (roles[roleName]) return;
481
- const available = Object.keys(roles);
482
- const list = available.length > 0 ? available.join(", ") : "(none defined)";
483
- throw new Error(`[flue] Role "${roleName}" not registered. Available roles: ${list}. Define roles as markdown files in \`roles/\` (or \`.flue/roles/\` if your root uses the .flue/ source layout).`);
484
- }
485
- function resolveEffectiveRole(options) {
486
- const role = options.callRole ?? options.sessionRole ?? options.agentRole;
487
- assertRoleExists(options.roles, role);
488
- return role;
489
- }
490
- function resolveRoleModel(roles, roleName) {
491
- assertRoleExists(roles, roleName);
492
- return roleName ? roles[roleName]?.model : void 0;
493
- }
494
- function resolveRoleThinkingLevel(roles, roleName) {
495
- assertRoleExists(roles, roleName);
496
- return roleName ? roles[roleName]?.thinkingLevel : void 0;
497
- }
498
-
499
499
  //#endregion
500
500
  //#region src/session.ts
501
501
  const MAX_TASK_DEPTH = 4;
@@ -652,7 +652,7 @@ var SessionHistory = class SessionHistory {
652
652
  }
653
653
  toData(metadata, createdAt, updatedAt) {
654
654
  return {
655
- version: 2,
655
+ version: 3,
656
656
  entries: [...this.entries],
657
657
  leafId: this.leafId,
658
658
  metadata,
@@ -703,7 +703,7 @@ function generateEntryId(byId) {
703
703
  return crypto.randomUUID();
704
704
  }
705
705
  var Session = class {
706
- id;
706
+ name;
707
707
  fs;
708
708
  metadata;
709
709
  get role() {
@@ -723,13 +723,16 @@ var Session = class {
723
723
  agentTools;
724
724
  deleted = false;
725
725
  activeOperation;
726
+ activeOperationId;
727
+ toolStartTimes = /* @__PURE__ */ new Map();
728
+ turnStartTime;
726
729
  activeTasks = /* @__PURE__ */ new Set();
727
730
  sessionRole;
728
731
  taskDepth;
729
732
  createTaskSession;
730
733
  onDelete;
731
734
  constructor(options) {
732
- this.id = options.id;
735
+ this.name = options.name;
733
736
  this.storageKey = options.storageKey;
734
737
  this.config = options.config;
735
738
  this.env = options.env;
@@ -764,13 +767,15 @@ var Session = class {
764
767
  },
765
768
  getApiKey: (provider) => this.getProviderApiKey(provider),
766
769
  onPayload: (payload, model) => this.applyProviderPayloadOverrides(payload, model),
767
- toolExecution: "parallel"
770
+ toolExecution: "parallel",
771
+ sessionId: options.affinityKey
768
772
  });
769
773
  this.eventCallback = options.onAgentEvent;
770
774
  this.harness.subscribe(async (event) => {
771
775
  switch (event.type) {
772
- case "agent_start":
773
- this.emit({ type: "agent_start" });
776
+ case "agent_start": break;
777
+ case "turn_start":
778
+ this.turnStartTime = Date.now();
774
779
  break;
775
780
  case "message_update": {
776
781
  const aEvent = event.assistantMessageEvent;
@@ -790,6 +795,7 @@ var Session = class {
790
795
  break;
791
796
  }
792
797
  case "tool_execution_start":
798
+ this.toolStartTimes.set(event.toolCallId, Date.now());
793
799
  this.emit({
794
800
  type: "tool_start",
795
801
  toolName: event.toolName,
@@ -799,16 +805,30 @@ var Session = class {
799
805
  break;
800
806
  case "tool_execution_end":
801
807
  this.emit({
802
- type: "tool_end",
808
+ type: "tool_call",
803
809
  toolName: event.toolName,
804
810
  toolCallId: event.toolCallId,
805
811
  isError: event.isError,
806
- result: event.result
812
+ result: event.result,
813
+ durationMs: durationSince(this.toolStartTimes.get(event.toolCallId))
807
814
  });
815
+ this.toolStartTimes.delete(event.toolCallId);
808
816
  break;
809
- case "turn_end":
810
- this.emit({ type: "turn_end" });
817
+ case "turn_end": {
818
+ const message = event.message;
819
+ const assistant = message.role === "assistant" ? message : void 0;
820
+ this.emit({
821
+ type: "turn",
822
+ durationMs: durationSince(this.turnStartTime),
823
+ model: assistant?.model,
824
+ usage: fromProviderUsage(assistant?.usage),
825
+ stopReason: assistant?.stopReason,
826
+ isError: assistant?.stopReason === "error" || assistant?.stopReason === "aborted",
827
+ error: assistant?.errorMessage
828
+ });
829
+ this.turnStartTime = void 0;
811
830
  break;
831
+ }
812
832
  case "agent_end": break;
813
833
  }
814
834
  });
@@ -870,6 +890,7 @@ var Session = class {
870
890
  shell(command, options) {
871
891
  return createCallHandle(options?.signal, (signal) => this.runOperation("shell", signal, async () => {
872
892
  const toolCallId = crypto.randomUUID();
893
+ const toolStartMs = Date.now();
873
894
  const args = { command };
874
895
  if (options?.cwd !== void 0) args.cwd = options.cwd;
875
896
  if (options?.env !== void 0) args.env = redactEnvValues(options.env);
@@ -893,11 +914,12 @@ var Session = class {
893
914
  const toolResult = formatBashResult(shellResult, command);
894
915
  await this.appendShellTriple(toolCallId, args, toolResult, false);
895
916
  this.emit({
896
- type: "tool_end",
917
+ type: "tool_call",
897
918
  toolName: "bash",
898
919
  toolCallId,
899
920
  isError: false,
900
- result: toolResult
921
+ result: toolResult,
922
+ durationMs: durationSince(toolStartMs)
901
923
  });
902
924
  return shellResult;
903
925
  } catch (error) {
@@ -913,11 +935,12 @@ var Session = class {
913
935
  };
914
936
  await this.appendShellTriple(toolCallId, args, errResult, true);
915
937
  this.emit({
916
- type: "tool_end",
938
+ type: "tool_call",
917
939
  toolName: "bash",
918
940
  toolCallId,
919
941
  isError: true,
920
- result: errResult
942
+ result: errResult,
943
+ durationMs: durationSince(toolStartMs)
921
944
  });
922
945
  throw error;
923
946
  }
@@ -1070,7 +1093,7 @@ var Session = class {
1070
1093
  }],
1071
1094
  details: {
1072
1095
  taskId: result.taskId,
1073
- sessionId: result.sessionId,
1096
+ session: result.session,
1074
1097
  messageId: result.messageId,
1075
1098
  role: result.role,
1076
1099
  cwd: result.cwd
@@ -1078,6 +1101,46 @@ var Session = class {
1078
1101
  };
1079
1102
  }
1080
1103
  async runTask(text, options, signal) {
1104
+ return this.runExclusive("task", async () => {
1105
+ if (signal?.aborted) throw abortErrorFor(signal);
1106
+ this.activeOperationId = generateOperationId();
1107
+ const operationId = this.activeOperationId;
1108
+ const startedAt = Date.now();
1109
+ this.emit({
1110
+ type: "operation_start",
1111
+ operationId,
1112
+ operationKind: "task"
1113
+ });
1114
+ try {
1115
+ const result = await this.runTaskExclusive(text, options, signal);
1116
+ this.emit({
1117
+ type: "operation",
1118
+ operationId,
1119
+ operationKind: "task",
1120
+ durationMs: durationSince(startedAt),
1121
+ isError: false,
1122
+ result: result.output,
1123
+ usage: usageFromResult(result.output)
1124
+ });
1125
+ return result;
1126
+ } catch (error) {
1127
+ const surfaced = signal?.aborted ? abortErrorFor(signal) : error;
1128
+ this.emit({
1129
+ type: "operation",
1130
+ operationId,
1131
+ operationKind: "task",
1132
+ durationMs: durationSince(startedAt),
1133
+ isError: true,
1134
+ error: serializeError(surfaced)
1135
+ });
1136
+ throw surfaced;
1137
+ } finally {
1138
+ this.emit({ type: "idle" });
1139
+ this.activeOperationId = void 0;
1140
+ }
1141
+ });
1142
+ }
1143
+ async runTaskExclusive(text, options, signal) {
1081
1144
  this.assertActive();
1082
1145
  if (!this.createTaskSession) throw new Error("[flue] This session cannot create task sessions.");
1083
1146
  if (this.taskDepth >= MAX_TASK_DEPTH) throw new Error(`[flue] Maximum task depth (${MAX_TASK_DEPTH}) exceeded.`);
@@ -1092,19 +1155,20 @@ var Session = class {
1092
1155
  prompt: text,
1093
1156
  role: requestedRole,
1094
1157
  cwd: options?.cwd,
1095
- parentSessionId: this.id
1158
+ parentSession: this.name
1096
1159
  });
1160
+ const taskStartMs = Date.now();
1097
1161
  try {
1098
1162
  const role = this.resolveEffectiveRole(options?.role);
1099
1163
  child = await this.createTaskSession({
1100
- parentSessionId: this.id,
1164
+ parentSession: this.name,
1101
1165
  taskId,
1102
1166
  parentEnv: this.env,
1103
1167
  cwd: options?.cwd,
1104
1168
  role,
1105
1169
  depth: this.taskDepth + 1
1106
1170
  });
1107
- await this.recordTaskSession(child.id, child.storageKey, taskId);
1171
+ await this.recordTaskSession(child.name, child.storageKey, taskId);
1108
1172
  this.activeTasks.add(child);
1109
1173
  if (signal) {
1110
1174
  abortListener = () => child?.abort();
@@ -1126,26 +1190,28 @@ var Session = class {
1126
1190
  output,
1127
1191
  text: typeof output?.text === "string" ? output.text : child.getAssistantText(),
1128
1192
  taskId,
1129
- sessionId: child.id,
1193
+ session: child.name,
1130
1194
  messageId: child.getLatestAssistantMessageId(),
1131
1195
  role,
1132
1196
  cwd: options?.cwd
1133
1197
  };
1134
1198
  this.emit({
1135
- type: "task_end",
1199
+ type: "task",
1136
1200
  taskId,
1137
1201
  isError: false,
1138
1202
  result: taskResult.text,
1139
- parentSessionId: this.id
1203
+ durationMs: durationSince(taskStartMs),
1204
+ parentSession: this.name
1140
1205
  });
1141
1206
  return taskResult;
1142
1207
  } catch (error) {
1143
1208
  this.emit({
1144
- type: "task_end",
1209
+ type: "task",
1145
1210
  taskId,
1146
1211
  isError: true,
1147
1212
  result: getErrorMessage(error),
1148
- parentSessionId: this.id
1213
+ durationMs: durationSince(taskStartMs),
1214
+ parentSession: this.name
1149
1215
  });
1150
1216
  throw error;
1151
1217
  } finally {
@@ -1159,6 +1225,14 @@ var Session = class {
1159
1225
  async runOperation(operation, signal, fn) {
1160
1226
  return this.runExclusive(operation, async () => {
1161
1227
  if (signal?.aborted) throw abortErrorFor(signal);
1228
+ this.activeOperationId = generateOperationId();
1229
+ const operationId = this.activeOperationId;
1230
+ const startedAt = Date.now();
1231
+ this.emit({
1232
+ type: "operation_start",
1233
+ operationId,
1234
+ operationKind: operation
1235
+ });
1162
1236
  const onAbort = () => {
1163
1237
  this.harness.abort();
1164
1238
  this.compactionAbortController?.abort(signal?.reason);
@@ -1166,18 +1240,38 @@ var Session = class {
1166
1240
  };
1167
1241
  signal?.addEventListener("abort", onAbort, { once: true });
1168
1242
  try {
1169
- return await fn();
1243
+ const result = await fn();
1244
+ this.emit({
1245
+ type: "operation",
1246
+ operationId,
1247
+ operationKind: operation,
1248
+ durationMs: durationSince(startedAt),
1249
+ isError: false,
1250
+ result,
1251
+ usage: usageFromResult(result)
1252
+ });
1253
+ return result;
1170
1254
  } catch (error) {
1171
- throw signal?.aborted ? abortErrorFor(signal) : error;
1255
+ const surfaced = signal?.aborted ? abortErrorFor(signal) : error;
1256
+ this.emit({
1257
+ type: "operation",
1258
+ operationId,
1259
+ operationKind: operation,
1260
+ durationMs: durationSince(startedAt),
1261
+ isError: true,
1262
+ error: serializeError(surfaced)
1263
+ });
1264
+ throw surfaced;
1172
1265
  } finally {
1173
1266
  signal?.removeEventListener("abort", onAbort);
1174
1267
  this.emit({ type: "idle" });
1268
+ this.activeOperationId = void 0;
1175
1269
  }
1176
1270
  });
1177
1271
  }
1178
1272
  async runExclusive(operation, fn) {
1179
1273
  this.assertActive();
1180
- if (this.activeOperation) throw new Error(`[flue] Session "${this.id}" is already running ${this.activeOperation}. Start another session for parallel conversation branches.`);
1274
+ if (this.activeOperation) throw new Error(`[flue] Session "${this.name}" is already running ${this.activeOperation}. Start another session for parallel conversation branches.`);
1181
1275
  this.activeOperation = operation;
1182
1276
  try {
1183
1277
  return await fn();
@@ -1186,27 +1280,18 @@ var Session = class {
1186
1280
  }
1187
1281
  }
1188
1282
  emit(event) {
1189
- this.eventCallback?.({
1283
+ const decorated = {
1190
1284
  ...event,
1191
- sessionId: this.id
1192
- });
1285
+ session: event.session ?? this.name
1286
+ };
1287
+ const operationId = event.operationId ?? this.activeOperationId;
1288
+ if (operationId !== void 0) decorated.operationId = operationId;
1289
+ this.eventCallback?.(decorated);
1193
1290
  }
1194
1291
  assertActive() {
1195
- if (this.deleted) throw new Error(`[flue] Session "${this.id}" has been deleted.`);
1292
+ if (this.deleted) throw new Error(`[flue] Session "${this.name}" has been deleted.`);
1196
1293
  }
1197
- /**
1198
- * Append the three-message conversational triple that represents a
1199
- * `session.shell()` call in the message history:
1200
- *
1201
- * 1. user — out-of-band request to run the command
1202
- * 2. assistant — synthetic turn whose content is a single bash
1203
- * tool_use block (matching the shape pi-ai's
1204
- * providers produce when the LLM itself calls bash)
1205
- * 3. toolResult — the bash output, keyed to the same toolCallId
1206
- *
1207
- * This makes a session.shell() call indistinguishable from an
1208
- * LLM-issued bash tool call when later turns read the transcript.
1209
- */
1294
+ /** Append a `session.shell()` call as an LLM-shaped bash tool exchange. */
1210
1295
  async appendShellTriple(toolCallId, args, toolResult, isError) {
1211
1296
  const timestamp = Date.now();
1212
1297
  const userMessage = {
@@ -1271,11 +1356,11 @@ var Session = class {
1271
1356
  if (!this.createdAt) this.createdAt = now;
1272
1357
  await this.store.save(this.storageKey, data);
1273
1358
  }
1274
- async recordTaskSession(sessionId, storageKey, taskId) {
1359
+ async recordTaskSession(session, storageKey, taskId) {
1275
1360
  const taskSessions = Array.isArray(this.metadata.taskSessions) ? this.metadata.taskSessions : [];
1276
- if (!taskSessions.some((task) => task?.sessionId === sessionId)) {
1361
+ if (!taskSessions.some((task) => task?.session === session)) {
1277
1362
  taskSessions.push({
1278
- sessionId,
1363
+ session,
1279
1364
  taskId,
1280
1365
  storageKey
1281
1366
  });
@@ -1295,7 +1380,7 @@ var Session = class {
1295
1380
  if (isContextOverflow(assistantMessage, contextWindow)) {
1296
1381
  if (this.overflowRecoveryAttempted) return;
1297
1382
  this.overflowRecoveryAttempted = true;
1298
- console.error(`[flue:compaction] Overflow detected, compacting and retrying...`);
1383
+ this.internalLog("info", "[flue:compaction] Overflow detected, compacting and retrying...");
1299
1384
  const messages = this.harness.state.messages;
1300
1385
  const lastMsg = messages[messages.length - 1];
1301
1386
  if (lastMsg && lastMsg.role === "assistant") {
@@ -1313,7 +1398,7 @@ var Session = class {
1313
1398
  if (assistantMessage.stopReason === "error") return;
1314
1399
  const contextTokens = calculateContextTokens(assistantMessage.usage);
1315
1400
  if (shouldCompact(contextTokens, contextWindow, this.compactionSettings)) {
1316
- console.error(`[flue:compaction] Threshold reached — ${contextTokens} tokens used, window ${contextWindow}, reserve ${this.compactionSettings.reserveTokens}, triggering compaction`);
1401
+ this.internalLog("info", `[flue:compaction] Threshold reached — ${contextTokens} tokens used, window ${contextWindow}, reserve ${this.compactionSettings.reserveTokens}, triggering compaction`);
1317
1402
  await this.runCompaction("threshold", false);
1318
1403
  }
1319
1404
  }
@@ -1327,6 +1412,7 @@ var Session = class {
1327
1412
  async runCompaction(reason, willRetry) {
1328
1413
  this.compactionAbortController = new AbortController();
1329
1414
  const messagesBefore = this.harness.state.messages.length;
1415
+ const compactionStartMs = Date.now();
1330
1416
  try {
1331
1417
  const model = this.harness.state.model;
1332
1418
  const contextEntries = this.history.buildContextEntries();
@@ -1338,15 +1424,15 @@ var Session = class {
1338
1424
  details: latestCompaction.details
1339
1425
  } : void 0);
1340
1426
  if (!preparation) {
1341
- console.error(`[flue:compaction] Nothing to compact (no valid cut point found)`);
1427
+ this.internalLog("info", "[flue:compaction] Nothing to compact (no valid cut point found)");
1342
1428
  return;
1343
1429
  }
1344
1430
  const firstKeptEntry = contextEntries[preparation.firstKeptIndex]?.entry;
1345
1431
  if (!firstKeptEntry || firstKeptEntry.type !== "message") {
1346
- console.error(`[flue:compaction] Nothing to compact (first kept message has no entry)`);
1432
+ this.internalLog("info", "[flue:compaction] Nothing to compact (first kept message has no entry)");
1347
1433
  return;
1348
1434
  }
1349
- console.error(`[flue:compaction] Summarizing ${preparation.messagesToSummarize.length} messages` + (preparation.isSplitTurn ? ` (split turn: ${preparation.turnPrefixMessages.length} prefix messages)` : "") + `, keeping messages from index ${preparation.firstKeptIndex}`);
1435
+ this.internalLog("info", `[flue:compaction] Summarizing ${preparation.messagesToSummarize.length} messages` + (preparation.isSplitTurn ? ` (split turn: ${preparation.turnPrefixMessages.length} prefix messages)` : "") + `, keeping messages from index ${preparation.firstKeptIndex}`);
1350
1436
  const estimatedTokens = preparation.tokensBefore;
1351
1437
  this.emit({
1352
1438
  type: "compaction_start",
@@ -1364,18 +1450,20 @@ var Session = class {
1364
1450
  });
1365
1451
  this.harness.state.messages = this.history.buildContext();
1366
1452
  const messagesAfter = this.harness.state.messages.length;
1367
- console.error(`[flue:compaction] Complete — messages: ${messagesBefore} → ${messagesAfter}, tokens before: ${result.tokensBefore}`);
1453
+ this.internalLog("info", `[flue:compaction] Complete — messages: ${messagesBefore} → ${messagesAfter}, tokens before: ${result.tokensBefore}`);
1368
1454
  this.emit({
1369
- type: "compaction_end",
1455
+ type: "compaction",
1370
1456
  messagesBefore,
1371
- messagesAfter
1457
+ messagesAfter,
1458
+ durationMs: durationSince(compactionStartMs),
1459
+ usage: result.usage
1372
1460
  });
1373
1461
  await this.save();
1374
1462
  if (willRetry) {
1375
1463
  const msgs = this.harness.state.messages;
1376
1464
  const lastMsg = msgs[msgs.length - 1];
1377
1465
  if (lastMsg?.role === "assistant" && lastMsg.stopReason === "error") this.harness.state.messages = msgs.slice(0, -1);
1378
- console.error(`[flue:compaction] Retrying after overflow recovery...`);
1466
+ this.internalLog("info", "[flue:compaction] Retrying after overflow recovery...");
1379
1467
  const beforeRetry = this.harness.state.messages.length;
1380
1468
  await this.harness.continue();
1381
1469
  await this.harness.waitForIdle();
@@ -1383,11 +1471,20 @@ var Session = class {
1383
1471
  }
1384
1472
  } catch (error) {
1385
1473
  const errorMessage = error instanceof Error ? error.message : String(error);
1386
- console.error(`[flue:compaction] Failed: ${errorMessage}`);
1474
+ this.internalLog("error", `[flue:compaction] Failed: ${errorMessage}`, { error });
1387
1475
  } finally {
1388
1476
  this.compactionAbortController = void 0;
1389
1477
  }
1390
1478
  }
1479
+ internalLog(level, message, attributes) {
1480
+ console.error(message);
1481
+ this.emit({
1482
+ type: "log",
1483
+ level,
1484
+ message,
1485
+ attributes: normalizeLogAttributes(attributes)
1486
+ });
1487
+ }
1391
1488
  throwIfError(context) {
1392
1489
  const errorMsg = this.harness.state.errorMessage;
1393
1490
  if (errorMsg) throw new Error(`[flue] ${context} failed: ${errorMsg}`);
@@ -1530,6 +1627,32 @@ async function deleteSessionTree(store, storageKey, seen = /* @__PURE__ */ new S
1530
1627
  function getErrorMessage(error) {
1531
1628
  return error instanceof Error ? error.message : String(error);
1532
1629
  }
1630
+ function serializeError(error) {
1631
+ if (error instanceof Error) return {
1632
+ name: error.name,
1633
+ message: error.message
1634
+ };
1635
+ return error;
1636
+ }
1637
+ function normalizeLogAttributes(attributes) {
1638
+ if (!attributes) return void 0;
1639
+ if (!(attributes.error instanceof Error)) return attributes;
1640
+ return {
1641
+ ...attributes,
1642
+ error: serializeError(attributes.error)
1643
+ };
1644
+ }
1645
+ function durationSince(start) {
1646
+ return start === void 0 ? 0 : Date.now() - start;
1647
+ }
1648
+ function usageFromResult(result) {
1649
+ if (typeof result !== "object" || result === null) return void 0;
1650
+ const usage = result.usage;
1651
+ return isPromptUsage(usage) ? usage : void 0;
1652
+ }
1653
+ function isPromptUsage(value) {
1654
+ return typeof value === "object" && value !== null && typeof value.input === "number" && typeof value.output === "number" && typeof value.totalTokens === "number";
1655
+ }
1533
1656
  function redactEnvValues(env) {
1534
1657
  return Object.fromEntries(Object.keys(env).map((key) => [key, "<redacted>"]));
1535
1658
  }