@granular-software/sdk 0.4.50 → 0.4.51

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.
@@ -51,11 +51,11 @@ var __export = (target, all) => {
51
51
  for (var name in all)
52
52
  __defProp(target, name, { get: all[name], enumerable: true });
53
53
  };
54
- var __copyProps = (to, from, except, desc) => {
55
- if (from && typeof from === "object" || typeof from === "function") {
56
- for (let key of __getOwnPropNames(from))
54
+ var __copyProps = (to, from2, except, desc) => {
55
+ if (from2 && typeof from2 === "object" || typeof from2 === "function") {
56
+ for (let key of __getOwnPropNames(from2))
57
57
  if (!__hasOwnProp.call(to, key) && key !== except)
58
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
58
+ __defProp(to, key, { get: () => from2[key], enumerable: !(desc = __getOwnPropDesc(from2, key)) || desc.enumerable });
59
59
  }
60
60
  return to;
61
61
  };
@@ -4072,16 +4072,37 @@ var WSClient = class {
4072
4072
  tokenRefreshTimer = null;
4073
4073
  isExplicitlyDisconnected = false;
4074
4074
  reconnectAttempts = 0;
4075
+ connectPromise = null;
4076
+ connectionEpoch = 0;
4077
+ cancelConnectAttempt = null;
4075
4078
  options;
4076
4079
  constructor(options) {
4077
4080
  this.options = options;
4078
4081
  this.url = options.url;
4079
4082
  this.sessionId = options.sessionId;
4080
4083
  this.token = options.token;
4084
+ if (options.initialDocumentSnapshot) {
4085
+ this.seedDocumentSnapshot(options.initialDocumentSnapshot);
4086
+ }
4081
4087
  }
4082
4088
  get currentSessionId() {
4083
4089
  return this.sessionId;
4084
4090
  }
4091
+ seedDocumentSnapshot(document) {
4092
+ if (!document || typeof document !== "object" || Array.isArray(document)) {
4093
+ return;
4094
+ }
4095
+ try {
4096
+ this.doc = document instanceof Uint8Array ? Automerge__namespace.load(document) : Automerge__namespace.from(document);
4097
+ this.syncState = Automerge__namespace.initSyncState();
4098
+ this.emit("sync", this.doc);
4099
+ } catch (error) {
4100
+ console.warn("[Granular] Failed to seed cached session document", error);
4101
+ }
4102
+ }
4103
+ saveDocumentSnapshot() {
4104
+ return Automerge__namespace.save(this.doc);
4105
+ }
4085
4106
  clearTokenRefreshTimer() {
4086
4107
  if (this.tokenRefreshTimer) {
4087
4108
  clearTimeout(this.tokenRefreshTimer);
@@ -4191,8 +4212,23 @@ var WSClient = class {
4191
4212
  * Connect to the WebSocket server
4192
4213
  * @returns {Promise<void>} Resolves when connection is open
4193
4214
  */
4194
- async connect() {
4215
+ async connect(options = {}) {
4216
+ if (this.ws?.readyState === READY_STATE_OPEN) return;
4217
+ if (this.connectPromise) return this.connectPromise;
4218
+ const connectPromise = this.connectAttempt(options.signal);
4219
+ this.connectPromise = connectPromise;
4220
+ try {
4221
+ await connectPromise;
4222
+ } finally {
4223
+ if (this.connectPromise === connectPromise) {
4224
+ this.connectPromise = null;
4225
+ }
4226
+ }
4227
+ }
4228
+ async connectAttempt(signal) {
4229
+ if (signal?.aborted) throw new Error("WebSocket connect aborted");
4195
4230
  const token = await this.resolveTokenForConnect();
4231
+ if (signal?.aborted) throw new Error("WebSocket connect aborted");
4196
4232
  this.isExplicitlyDisconnected = false;
4197
4233
  this.scheduleTokenRefresh();
4198
4234
  if (this.reconnectTimer) {
@@ -4204,7 +4240,7 @@ var WSClient = class {
4204
4240
  try {
4205
4241
  const wsModule = await Promise.resolve().then(() => (init_wrapper(), wrapper_exports));
4206
4242
  WebSocketClass = wsModule.default || wsModule;
4207
- } catch (e) {
4243
+ } catch {
4208
4244
  }
4209
4245
  }
4210
4246
  if (!WebSocketClass) {
@@ -4212,83 +4248,97 @@ var WSClient = class {
4212
4248
  'No WebSocket implementation found. If using Node.js, please install "ws" and pass the constructor to the SDK options: { WebSocketCtor: WebSocket }.'
4213
4249
  );
4214
4250
  }
4251
+ const epoch = ++this.connectionEpoch;
4252
+ const wsUrl = new URL(this.url);
4253
+ wsUrl.searchParams.set("sessionId", this.sessionId);
4254
+ wsUrl.searchParams.set("token", token);
4255
+ const socket = new WebSocketClass(wsUrl.toString());
4256
+ this.ws = socket;
4215
4257
  return new Promise((resolve, reject) => {
4216
- try {
4217
- const wsUrl = new URL(this.url);
4218
- wsUrl.searchParams.set("sessionId", this.sessionId);
4219
- wsUrl.searchParams.set("token", token);
4220
- this.ws = new WebSocketClass(wsUrl.toString());
4221
- if (!this.ws) throw new Error("Failed to create WebSocket");
4222
- const socket = this.ws;
4223
- if (typeof socket.on === "function") {
4224
- socket.on("open", () => {
4225
- if (this.reconnectTimer) {
4226
- clearTimeout(this.reconnectTimer);
4227
- this.reconnectTimer = null;
4228
- }
4229
- this.reconnectAttempts = 0;
4230
- this.emit("open", {});
4231
- resolve();
4232
- });
4233
- socket.on("message", (data) => {
4234
- try {
4235
- const message = JSON.parse(data.toString());
4236
- this.handleMessage(message);
4237
- } catch (error) {
4238
- console.error("[Granular] Failed to parse message:", error);
4239
- }
4240
- });
4241
- socket.on("error", (error) => {
4242
- this.emit("error", error);
4243
- if (socket.readyState !== READY_STATE_OPEN) {
4244
- reject(error);
4245
- }
4246
- });
4247
- socket.on("close", (code, reason) => {
4248
- this.handleDisconnect({
4249
- code,
4250
- reason: this.normalizeReason(reason),
4251
- // ws does not provide wasClean on Node-style close callback
4252
- wasClean: code === 1e3
4253
- });
4254
- });
4258
+ let settled = false;
4259
+ const isCurrent = () => this.connectionEpoch === epoch && this.ws === socket;
4260
+ const finish = (error) => {
4261
+ if (settled) return;
4262
+ settled = true;
4263
+ if (this.cancelConnectAttempt === handleAbort) {
4264
+ this.cancelConnectAttempt = null;
4265
+ }
4266
+ signal?.removeEventListener("abort", handleAbort);
4267
+ if (error) {
4268
+ reject(error instanceof Error ? error : new Error(String(error)));
4255
4269
  } else {
4256
- this.ws.onopen = () => {
4257
- if (this.reconnectTimer) {
4258
- clearTimeout(this.reconnectTimer);
4259
- this.reconnectTimer = null;
4260
- }
4261
- this.reconnectAttempts = 0;
4262
- this.emit("open", {});
4263
- resolve();
4264
- };
4265
- this.ws.onmessage = (event) => {
4266
- try {
4267
- const data = event.data;
4268
- const message = JSON.parse(data.toString());
4269
- this.handleMessage(message);
4270
- } catch (error) {
4271
- console.error("[Granular] Failed to parse message:", error);
4272
- }
4273
- };
4274
- this.ws.onerror = (event) => {
4275
- const error = new Error("WebSocket error");
4276
- error.event = event;
4277
- this.emit("error", error);
4278
- if (this.ws?.readyState !== READY_STATE_OPEN) {
4279
- reject(error);
4280
- }
4281
- };
4282
- this.ws.onclose = (event) => {
4283
- this.handleDisconnect({
4284
- code: event.code,
4285
- reason: event.reason,
4286
- wasClean: event.wasClean
4287
- });
4288
- };
4270
+ resolve();
4289
4271
  }
4290
- } catch (error) {
4291
- reject(error);
4272
+ };
4273
+ const closeStaleSocket = () => {
4274
+ try {
4275
+ socket.close(1e3, "Stale connection attempt");
4276
+ } catch {
4277
+ }
4278
+ };
4279
+ const handleAbort = () => {
4280
+ if (isCurrent()) {
4281
+ this.connectionEpoch += 1;
4282
+ this.ws = null;
4283
+ }
4284
+ closeStaleSocket();
4285
+ finish(new Error("WebSocket connect aborted"));
4286
+ };
4287
+ this.cancelConnectAttempt = handleAbort;
4288
+ const handleOpen = () => {
4289
+ if (!isCurrent()) {
4290
+ closeStaleSocket();
4291
+ return;
4292
+ }
4293
+ this.reconnectAttempts = 0;
4294
+ this.emit("open", {});
4295
+ finish();
4296
+ };
4297
+ const handleMessage = (data) => {
4298
+ if (!isCurrent()) return;
4299
+ try {
4300
+ const text = typeof data === "string" ? data : data && typeof data === "object" && "toString" in data ? String(data.toString()) : "";
4301
+ this.handleMessage(JSON.parse(text));
4302
+ } catch (error) {
4303
+ console.error("[Granular] Failed to parse message:", error);
4304
+ }
4305
+ };
4306
+ const handleError = (error) => {
4307
+ if (!isCurrent()) return;
4308
+ const typedError = error instanceof Error ? error : new Error("WebSocket error");
4309
+ this.emit("error", typedError);
4310
+ if (socket.readyState !== READY_STATE_OPEN) finish(typedError);
4311
+ };
4312
+ const handleClose = (close) => {
4313
+ if (!isCurrent()) return;
4314
+ if (!settled) {
4315
+ finish(
4316
+ new Error(
4317
+ `WebSocket closed before ready${close.code ? ` (code=${close.code})` : ""}`
4318
+ )
4319
+ );
4320
+ }
4321
+ this.handleDisconnect({
4322
+ code: close.code,
4323
+ reason: this.normalizeReason(close.reason),
4324
+ wasClean: close.wasClean
4325
+ });
4326
+ };
4327
+ signal?.addEventListener("abort", handleAbort, { once: true });
4328
+ const nodeSocket = socket;
4329
+ if (typeof nodeSocket.on === "function") {
4330
+ nodeSocket.on("open", handleOpen);
4331
+ nodeSocket.on("message", handleMessage);
4332
+ nodeSocket.on("error", handleError);
4333
+ nodeSocket.on(
4334
+ "close",
4335
+ (code, reason) => handleClose({ code, reason, wasClean: code === 1e3 })
4336
+ );
4337
+ } else {
4338
+ socket.onopen = handleOpen;
4339
+ socket.onmessage = (event) => handleMessage(event.data);
4340
+ socket.onerror = handleError;
4341
+ socket.onclose = (event) => handleClose(event);
4292
4342
  }
4293
4343
  });
4294
4344
  }
@@ -4306,9 +4356,58 @@ var WSClient = class {
4306
4356
  return void 0;
4307
4357
  }
4308
4358
  rejectPending(error) {
4309
- this.messageQueue.forEach((pending) => pending.reject(error));
4359
+ this.messageQueue.forEach((pending) => {
4360
+ clearTimeout(pending.timeout);
4361
+ pending.reject(error);
4362
+ });
4310
4363
  this.messageQueue = [];
4311
4364
  }
4365
+ emitReconnectErrorMessage(error) {
4366
+ const reconnectInfo = {
4367
+ error,
4368
+ sessionId: this.sessionId,
4369
+ timestamp: Date.now()
4370
+ };
4371
+ this.emit("reconnect_error", reconnectInfo);
4372
+ if (this.options.onReconnectError) {
4373
+ try {
4374
+ this.options.onReconnectError(reconnectInfo);
4375
+ } catch (callbackError) {
4376
+ console.error(
4377
+ "[Granular] onReconnectError callback failed:",
4378
+ callbackError
4379
+ );
4380
+ }
4381
+ }
4382
+ }
4383
+ scheduleReconnectAttempt() {
4384
+ if (this.isExplicitlyDisconnected || this.reconnectTimer) return null;
4385
+ const baseReconnectDelayMs = typeof this.options.reconnectDelayMs === "number" && Number.isFinite(this.options.reconnectDelayMs) && this.options.reconnectDelayMs > 0 ? this.options.reconnectDelayMs : DEFAULT_RECONNECT_DELAY_MS;
4386
+ const maxReconnectAttempts = typeof this.options.maxReconnectAttempts === "number" && Number.isFinite(this.options.maxReconnectAttempts) && this.options.maxReconnectAttempts >= 0 ? Math.floor(this.options.maxReconnectAttempts) : DEFAULT_MAX_RECONNECT_ATTEMPTS;
4387
+ if (this.reconnectAttempts >= maxReconnectAttempts) {
4388
+ this.emitReconnectErrorMessage(
4389
+ `WebSocket reconnect attempts exhausted after ${maxReconnectAttempts} attempt(s).`
4390
+ );
4391
+ return null;
4392
+ }
4393
+ this.reconnectAttempts += 1;
4394
+ const reconnectDelayMs = Math.min(
4395
+ 3e4,
4396
+ baseReconnectDelayMs * 2 ** Math.max(0, this.reconnectAttempts - 1)
4397
+ );
4398
+ this.reconnectTimer = setTimeout(() => {
4399
+ this.reconnectTimer = null;
4400
+ console.log("[Granular] Attempting reconnect...");
4401
+ this.connect().catch((error) => {
4402
+ console.error("[Granular] Reconnect failed:", error);
4403
+ this.emitReconnectErrorMessage(
4404
+ error instanceof Error ? error.message : String(error)
4405
+ );
4406
+ this.scheduleReconnectAttempt();
4407
+ });
4408
+ }, reconnectDelayMs);
4409
+ return reconnectDelayMs;
4410
+ }
4312
4411
  buildDisconnectError(info) {
4313
4412
  const details = [
4314
4413
  info.code !== void 0 ? `code=${info.code}` : void 0,
@@ -4318,8 +4417,6 @@ var WSClient = class {
4318
4417
  return new Error(`WebSocket disconnected${suffix}`);
4319
4418
  }
4320
4419
  handleDisconnect(close = {}) {
4321
- const baseReconnectDelayMs = typeof this.options.reconnectDelayMs === "number" && Number.isFinite(this.options.reconnectDelayMs) && this.options.reconnectDelayMs > 0 ? this.options.reconnectDelayMs : DEFAULT_RECONNECT_DELAY_MS;
4322
- const maxReconnectAttempts = typeof this.options.maxReconnectAttempts === "number" && Number.isFinite(this.options.maxReconnectAttempts) && this.options.maxReconnectAttempts >= 0 ? Math.floor(this.options.maxReconnectAttempts) : DEFAULT_MAX_RECONNECT_ATTEMPTS;
4323
4420
  const unexpected = !this.isExplicitlyDisconnected;
4324
4421
  const info = {
4325
4422
  code: close.code,
@@ -4339,32 +4436,9 @@ var WSClient = class {
4339
4436
  const disconnectError = this.buildDisconnectError(info);
4340
4437
  this.rejectPending(disconnectError);
4341
4438
  this.emit("disconnect", info);
4342
- if (this.reconnectAttempts >= maxReconnectAttempts) {
4343
- const reconnectInfo = {
4344
- error: `WebSocket reconnect attempts exhausted after ${maxReconnectAttempts} attempt(s).`,
4345
- sessionId: this.sessionId,
4346
- timestamp: Date.now()
4347
- };
4348
- this.emit("reconnect_error", reconnectInfo);
4349
- if (this.options.onReconnectError) {
4350
- try {
4351
- this.options.onReconnectError(reconnectInfo);
4352
- } catch (callbackError) {
4353
- console.error(
4354
- "[Granular] onReconnectError callback failed:",
4355
- callbackError
4356
- );
4357
- }
4358
- }
4359
- return;
4360
- }
4361
- this.reconnectAttempts += 1;
4362
- const reconnectDelayMs = Math.min(
4363
- 3e4,
4364
- baseReconnectDelayMs * 2 ** Math.max(0, this.reconnectAttempts - 1)
4365
- );
4366
- info.reconnectScheduled = true;
4367
- info.reconnectDelayMs = reconnectDelayMs;
4439
+ const reconnectDelayMs = this.scheduleReconnectAttempt();
4440
+ info.reconnectScheduled = reconnectDelayMs !== null;
4441
+ if (reconnectDelayMs !== null) info.reconnectDelayMs = reconnectDelayMs;
4368
4442
  if (this.options.onUnexpectedClose) {
4369
4443
  try {
4370
4444
  this.options.onUnexpectedClose(info);
@@ -4375,28 +4449,6 @@ var WSClient = class {
4375
4449
  );
4376
4450
  }
4377
4451
  }
4378
- this.reconnectTimer = setTimeout(() => {
4379
- console.log("[Granular] Attempting reconnect...");
4380
- this.connect().catch((error) => {
4381
- console.error("[Granular] Reconnect failed:", error);
4382
- const reconnectInfo = {
4383
- error: error instanceof Error ? error.message : String(error),
4384
- sessionId: this.sessionId,
4385
- timestamp: Date.now()
4386
- };
4387
- this.emit("reconnect_error", reconnectInfo);
4388
- if (this.options.onReconnectError) {
4389
- try {
4390
- this.options.onReconnectError(reconnectInfo);
4391
- } catch (callbackError) {
4392
- console.error(
4393
- "[Granular] onReconnectError callback failed:",
4394
- callbackError
4395
- );
4396
- }
4397
- }
4398
- });
4399
- }, reconnectDelayMs);
4400
4452
  }
4401
4453
  }
4402
4454
  handleMessage(message) {
@@ -4507,6 +4559,7 @@ var WSClient = class {
4507
4559
  const response = message;
4508
4560
  const pending = this.messageQueue.find((q) => q.id === response.id);
4509
4561
  if (pending) {
4562
+ clearTimeout(pending.timeout);
4510
4563
  if (response.type === "rpc_error") {
4511
4564
  pending.reject(
4512
4565
  new Error(
@@ -4552,16 +4605,22 @@ var WSClient = class {
4552
4605
  id
4553
4606
  };
4554
4607
  return new Promise((resolve, reject) => {
4555
- this.messageQueue.push({ resolve, reject, id });
4556
- this.ws.send(JSON.stringify(request));
4557
4608
  const timeoutMs = rpcTimeoutMsForMethod(method);
4558
- setTimeout(() => {
4609
+ const timeout = setTimeout(() => {
4559
4610
  const pending = this.messageQueue.find((q) => q.id === id);
4560
4611
  if (pending) {
4561
4612
  this.messageQueue = this.messageQueue.filter((q) => q.id !== id);
4562
4613
  reject(new Error(`RPC timeout: ${method}`));
4563
4614
  }
4564
4615
  }, timeoutMs);
4616
+ this.messageQueue.push({ resolve, reject, id, timeout });
4617
+ try {
4618
+ this.ws.send(JSON.stringify(request));
4619
+ } catch (error) {
4620
+ clearTimeout(timeout);
4621
+ this.messageQueue = this.messageQueue.filter((q) => q.id !== id);
4622
+ reject(error instanceof Error ? error : new Error(String(error)));
4623
+ }
4565
4624
  });
4566
4625
  }
4567
4626
  async handleIncomingRpc(request) {
@@ -4647,15 +4706,18 @@ var WSClient = class {
4647
4706
  /**
4648
4707
  * Disconnect the WebSocket and clear state
4649
4708
  */
4650
- disconnect() {
4709
+ disconnect(options = {}) {
4651
4710
  this.isExplicitlyDisconnected = true;
4711
+ this.cancelConnectAttempt?.();
4712
+ this.cancelConnectAttempt = null;
4713
+ this.connectionEpoch += 1;
4652
4714
  if (this.reconnectTimer) {
4653
4715
  clearTimeout(this.reconnectTimer);
4654
4716
  this.reconnectTimer = null;
4655
4717
  }
4656
4718
  this.clearTokenRefreshTimer();
4657
4719
  if (this.ws) {
4658
- this.ws.close(1e3, "Client disconnect");
4720
+ this.ws.close(1e3, options.reason || "Client disconnect");
4659
4721
  this.ws = null;
4660
4722
  }
4661
4723
  this.rejectPending(new Error("Client explicitly disconnected"));
@@ -4751,8 +4813,12 @@ function normalizePrompt(rawValue) {
4751
4813
  const source = promptRecord || raw;
4752
4814
  const id = typeof source.id === "string" ? source.id : typeof raw.id === "string" ? raw.id : typeof raw.promptId === "string" ? raw.promptId : "";
4753
4815
  if (!id) return null;
4816
+ const jobId = typeof source.jobId === "string" && source.jobId.trim() ? source.jobId.trim() : typeof raw.jobId === "string" && raw.jobId.trim() ? raw.jobId.trim() : void 0;
4817
+ const turnId = typeof source.turnId === "string" && source.turnId.trim() ? source.turnId.trim() : typeof raw.turnId === "string" && raw.turnId.trim() ? raw.turnId.trim() : void 0;
4754
4818
  return {
4755
4819
  id,
4820
+ ...jobId ? { jobId } : {},
4821
+ ...turnId ? { turnId } : {},
4756
4822
  type: normalizePromptType(source === raw ? raw : { ...raw, ...source }),
4757
4823
  title: typeof source.title === "string" ? source.title : "Input required",
4758
4824
  message: typeof source.message === "string" ? source.message : "",
@@ -4839,6 +4905,9 @@ var Session = class {
4839
4905
  this.initialQuota = options.initialQuota || null;
4840
4906
  this.setupEventHandlers();
4841
4907
  this.setupToolInvokeHandler();
4908
+ this.currentDomainRevision = this.extractDomainRevisionFromDoc(
4909
+ this.client.doc
4910
+ );
4842
4911
  }
4843
4912
  extractDomainRevisionFromDoc(doc) {
4844
4913
  const domain = doc?.domain;
@@ -5744,6 +5813,7 @@ function normalizeJobAgentMessageEnvelope(data) {
5744
5813
  }
5745
5814
  return {
5746
5815
  jobId: d.jobId,
5816
+ ...typeof d.turnId === "string" && d.turnId.trim() ? { turnId: d.turnId.trim() } : {},
5747
5817
  message: {
5748
5818
  messageId: d.messageId,
5749
5819
  kind: d.kind === "artifacts" ? "artifacts" : "text",
@@ -6402,9 +6472,14 @@ function normalizeShowRefs(value) {
6402
6472
  variableNames: normalizeRefs(record.variableNames),
6403
6473
  fileIds: normalizeRefs(record.fileIds),
6404
6474
  sessionArtifactIds: normalizeRefs(record.sessionArtifactIds),
6405
- actionSuggestions: normalizeActionSuggestions(record.actionSuggestions)
6475
+ actionSuggestions: normalizeActionSuggestions(record.actionSuggestions),
6476
+ tables: Array.isArray(record.tables) ? record.tables.filter(
6477
+ (table) => Boolean(
6478
+ table && typeof table === "object" && !Array.isArray(table) && Array.isArray(table.columns) && Array.isArray(table.rows)
6479
+ )
6480
+ ) : void 0
6406
6481
  };
6407
- return show.entryPaths || show.listNames || show.variableNames || show.fileIds || show.sessionArtifactIds || show.actionSuggestions ? show : void 0;
6482
+ return show.entryPaths || show.listNames || show.variableNames || show.fileIds || show.sessionArtifactIds || show.actionSuggestions || show.tables ? show : void 0;
6408
6483
  }
6409
6484
  function normalizeActionSuggestions(value) {
6410
6485
  if (!Array.isArray(value)) return void 0;
@@ -6426,6 +6501,76 @@ function normalizeActionSuggestions(value) {
6426
6501
  }
6427
6502
  return suggestions.length ? suggestions : void 0;
6428
6503
  }
6504
+ var TRANSCRIPT_MESSAGE_PART_LIMIT = 128;
6505
+ var TRANSCRIPT_MESSAGE_PART_TEXT_LIMIT = 2e5;
6506
+ var TRANSCRIPT_MESSAGE_PART_ACTION_LABEL_LIMIT = 1e3;
6507
+ var TRANSCRIPT_MESSAGE_ACTION_LIMIT = 64;
6508
+ function normalizeConversationMessageActions(value) {
6509
+ if (!Array.isArray(value) || value.length === 0) return void 0;
6510
+ const actions = [];
6511
+ for (const item of value.slice(0, TRANSCRIPT_MESSAGE_ACTION_LIMIT)) {
6512
+ const record = asRecord3(item);
6513
+ const kind = record?.kind;
6514
+ const label = trimString(record?.label ?? record?.title);
6515
+ const status = record?.status;
6516
+ if (kind !== "frontend" && kind !== "backend" && kind !== "system" || !label || label.length > TRANSCRIPT_MESSAGE_PART_ACTION_LABEL_LIMIT) {
6517
+ continue;
6518
+ }
6519
+ actions.push({
6520
+ kind,
6521
+ label,
6522
+ ...status === "done" || status === "queued" || status === "failed" ? { status } : {}
6523
+ });
6524
+ }
6525
+ return actions.length ? actions : void 0;
6526
+ }
6527
+ function normalizeConversationMessageParts(value, canonicalContent, canonicalActions) {
6528
+ if (!Array.isArray(value) || value.length === 0 || value.length > TRANSCRIPT_MESSAGE_PART_LIMIT) {
6529
+ return void 0;
6530
+ }
6531
+ const parts = [];
6532
+ const canonicalActionsById = new Map(
6533
+ (canonicalActions || []).map((action) => [
6534
+ `${action.kind}:${action.label}`,
6535
+ action
6536
+ ])
6537
+ );
6538
+ const seenActionIds = /* @__PURE__ */ new Set();
6539
+ let textLength = 0;
6540
+ for (const item of value) {
6541
+ const record = asRecord3(item);
6542
+ if (!record) return void 0;
6543
+ if (record.type === "text") {
6544
+ if (typeof record.text !== "string" || record.text.length === 0) {
6545
+ return void 0;
6546
+ }
6547
+ textLength += record.text.length;
6548
+ if (textLength > TRANSCRIPT_MESSAGE_PART_TEXT_LIMIT) return void 0;
6549
+ parts.push({ type: "text", text: record.text });
6550
+ continue;
6551
+ }
6552
+ if (record.type !== "action") return void 0;
6553
+ const action = asRecord3(record.action);
6554
+ const kind = action?.kind;
6555
+ const label = trimString(action?.label);
6556
+ if (kind !== "frontend" && kind !== "backend" && kind !== "system" || !label || label.length > TRANSCRIPT_MESSAGE_PART_ACTION_LABEL_LIMIT) {
6557
+ return void 0;
6558
+ }
6559
+ const actionId = `${kind}:${label}`;
6560
+ const canonicalAction = canonicalActionsById.get(actionId);
6561
+ if (!canonicalAction) return void 0;
6562
+ if (seenActionIds.has(actionId)) continue;
6563
+ seenActionIds.add(actionId);
6564
+ parts.push({
6565
+ type: "action",
6566
+ action: canonicalAction
6567
+ });
6568
+ }
6569
+ const orderedText = parts.filter(
6570
+ (part) => part.type === "text"
6571
+ ).map((part) => part.text).join("");
6572
+ return orderedText === canonicalContent ? parts : void 0;
6573
+ }
6429
6574
  function stringifyTranscriptValue(value, fallback = "") {
6430
6575
  if (typeof value === "string") {
6431
6576
  return value.trim() || fallback;
@@ -6584,10 +6729,12 @@ function normalizeConversationMessage(raw, artifactsById) {
6584
6729
  const content = trimString(
6585
6730
  record.content ?? record.reply ?? record.message ?? record.text
6586
6731
  );
6732
+ const actions = role === "assistant" ? normalizeConversationMessageActions(record.actions) : void 0;
6733
+ const parts = role === "assistant" ? normalizeConversationMessageParts(record.parts, content, actions) : void 0;
6587
6734
  const show = normalizeShowRefs(record.show);
6588
6735
  const id = asString(record.id) || crypto.randomUUID();
6589
6736
  const timestamp = asNumber(record.timestamp) || asNumber(record.ts) || 0;
6590
- if (!content && !show) return null;
6737
+ if (!content && !show && !actions?.length) return null;
6591
6738
  const artifactHistory = buildArtifactHistory(show, artifactsById);
6592
6739
  const historyContent = role === "assistant" ? content && artifactHistory ? `[Assistant reply]
6593
6740
  ${content}
@@ -6602,6 +6749,8 @@ ${content}` : artifactHistory : void 0;
6602
6749
  jobId: asString(record.jobId),
6603
6750
  promptId: asString(record.promptId),
6604
6751
  show,
6752
+ actions,
6753
+ parts,
6605
6754
  historyContent,
6606
6755
  source: "conversation"
6607
6756
  };
@@ -12179,7 +12328,12 @@ async function recordOpenAIUsageSpend(options) {
12179
12328
  const metadata = {
12180
12329
  ...options.metadata || {},
12181
12330
  ...options.usage.rawUsage !== void 0 ? { openaiUsage: options.usage.rawUsage } : {},
12182
- usageContext: context
12331
+ usageContext: context,
12332
+ pricingContextTier: options.usage.pricingContextTier,
12333
+ cacheWritePricePerMillionMicros: options.usage.cacheWritePricePerMillionMicros,
12334
+ cacheWriteTokens: options.usage.cacheWriteTokens,
12335
+ cacheWriteCostMicros: options.usage.cacheWriteCostMicros,
12336
+ longContextThresholdTokens: options.usage.longContextThresholdTokens
12183
12337
  };
12184
12338
  const response = await fetch(
12185
12339
  `${toGranularHttpBase(options.apiUrl)}/control/spend/events`,
@@ -13107,11 +13261,20 @@ function buildStateMachineModelMutations(modelPath, machines) {
13107
13261
  return mutations;
13108
13262
  }
13109
13263
  function buildMachineTypes(classSummary, machine) {
13264
+ const stateGlossary = machine.states.map((state) => {
13265
+ const label = state.label && state.label !== state.name ? state.label : null;
13266
+ const meaning = [label, state.description].filter(Boolean).join(" \u2014 ");
13267
+ const finalMarker = state.isFinal ? " Final state." : "";
13268
+ return `${state.name}${meaning ? `: ${meaning}` : "."}${finalMarker}`;
13269
+ });
13110
13270
  return [
13111
13271
  {
13112
13272
  kind: "union",
13113
13273
  name: stateTypeName(classSummary.name, machine.name),
13114
- docs: [`Allowed states for ${classSummary.name}.${machine.name}.`],
13274
+ docs: [
13275
+ `Allowed states for ${classSummary.name}.${machine.name}.`,
13276
+ ...stateGlossary
13277
+ ],
13115
13278
  members: machine.states.map((state) => state.name)
13116
13279
  },
13117
13280
  {
@@ -13459,7 +13622,7 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
13459
13622
  },
13460
13623
  add_transition: async (value, {
13461
13624
  name,
13462
- from,
13625
+ from: from2,
13463
13626
  to,
13464
13627
  label,
13465
13628
  description,
@@ -13474,7 +13637,7 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
13474
13637
  value.target.add_state_machine_transition(
13475
13638
  value.name,
13476
13639
  name,
13477
- from,
13640
+ from2,
13478
13641
  to,
13479
13642
  {
13480
13643
  label,
@@ -13826,6 +13989,18 @@ function buildEffectMetamodelMutations(toolPath, spec) {
13826
13989
  }
13827
13990
 
13828
13991
  // src/client.ts
13992
+ var DEFAULT_CONVERSATION_SESSION_LIST_LIMIT = 100;
13993
+ var MAX_CONVERSATION_SESSION_LIST_LIMIT = 500;
13994
+ var MAX_CONVERSATION_SESSION_LIST_OFFSET = 1e5;
13995
+ function boundedSessionListInteger(value, name, fallback, minimum, maximum) {
13996
+ if (value === void 0) return fallback;
13997
+ if (!Number.isInteger(value) || value < minimum || value > maximum) {
13998
+ throw new RangeError(
13999
+ `Session list ${name} must be an integer between ${minimum} and ${maximum}.`
14000
+ );
14001
+ }
14002
+ return value;
14003
+ }
13829
14004
  var STANDARD_MODULES_OPERATIONS = [
13830
14005
  {
13831
14006
  create: "entity",
@@ -14164,7 +14339,7 @@ var Environment = class _Environment {
14164
14339
  }
14165
14340
  get sessions() {
14166
14341
  return {
14167
- list: async (options) => this.listSessions(options?.status || "active"),
14342
+ list: async (options = {}) => this.listSessions(options),
14168
14343
  create: async (options) => this.createSession(options),
14169
14344
  connect: async (sessionId, options) => this.connectSession(sessionId, options),
14170
14345
  reopen: async (sessionId, options) => this.reopenSession(sessionId, options),
@@ -14299,17 +14474,12 @@ var Environment = class _Environment {
14299
14474
  */
14300
14475
  async disconnect() {
14301
14476
  }
14302
- async listSessions(status = "active") {
14303
- if (status === "all") {
14304
- const [active, closed] = await Promise.all([
14305
- this.granular.listOpenSessions({ environmentId: this.environmentId }),
14306
- this.granular.listClosedSessions({ environmentId: this.environmentId })
14307
- ]);
14308
- return [...active, ...closed].sort(
14309
- (left, right) => Date.parse(right.lastSeenAt) - Date.parse(left.lastSeenAt)
14310
- );
14311
- }
14312
- return status === "closed" ? this.granular.listClosedSessions({ environmentId: this.environmentId }) : this.granular.listOpenSessions({ environmentId: this.environmentId });
14477
+ async listSessions(optionsOrStatus = {}) {
14478
+ const options = typeof optionsOrStatus === "string" ? { status: optionsOrStatus } : optionsOrStatus;
14479
+ return this.granular.listSessions({
14480
+ ...options,
14481
+ environmentId: this.environmentId
14482
+ });
14313
14483
  }
14314
14484
  async getUserEnvironmentState(options = {}) {
14315
14485
  return this.granular.getUserEnvironmentState({
@@ -14327,6 +14497,7 @@ var Environment = class _Environment {
14327
14497
  return this.granular.createSession({
14328
14498
  environmentId: this.environmentId,
14329
14499
  clientId: options?.clientId,
14500
+ sessionScope: options?.sessionScope,
14330
14501
  initialHeap: options?.initialHeap
14331
14502
  });
14332
14503
  }
@@ -15889,7 +16060,7 @@ var EnvironmentSession = class extends Session {
15889
16060
  * Close only the socket transport without sending `client.goodbye`.
15890
16061
  */
15891
16062
  disconnectTransport() {
15892
- this.client.disconnect();
16063
+ this.client.disconnect({ reason: "Transport detach" });
15893
16064
  }
15894
16065
  /**
15895
16066
  * Backwards-compatible alias for `disconnect()`.
@@ -16284,16 +16455,71 @@ var Granular = class _Granular {
16284
16455
  };
16285
16456
  }
16286
16457
  /**
16287
- * List active (open) sessions for an environment each session is one agent conversation thread.
16458
+ * List indexed sessions using ownership filters and bounded pagination.
16459
+ */
16460
+ async listSessions(options) {
16461
+ const environmentId = options.environmentId?.trim();
16462
+ const sandboxId = options.sandboxId?.trim();
16463
+ const subjectId = options.subjectId?.trim();
16464
+ if (!environmentId && !sandboxId && !subjectId) {
16465
+ throw new Error(
16466
+ "listSessions() requires environmentId, sandboxId, or subjectId so history cannot be scanned accidentally."
16467
+ );
16468
+ }
16469
+ const status = options.status || "active";
16470
+ const allowedStatuses = /* @__PURE__ */ new Set([
16471
+ "active",
16472
+ "closed",
16473
+ "expired",
16474
+ "failed",
16475
+ "timeout",
16476
+ "all"
16477
+ ]);
16478
+ if (!allowedStatuses.has(status)) {
16479
+ throw new Error(`Unsupported session status: ${String(status)}`);
16480
+ }
16481
+ const limit = boundedSessionListInteger(
16482
+ options.limit,
16483
+ "limit",
16484
+ DEFAULT_CONVERSATION_SESSION_LIST_LIMIT,
16485
+ 1,
16486
+ MAX_CONVERSATION_SESSION_LIST_LIMIT
16487
+ );
16488
+ const offset = boundedSessionListInteger(
16489
+ options.offset,
16490
+ "offset",
16491
+ 0,
16492
+ 0,
16493
+ MAX_CONVERSATION_SESSION_LIST_OFFSET
16494
+ );
16495
+ const query = new URLSearchParams({
16496
+ limit: String(limit),
16497
+ offset: String(offset)
16498
+ });
16499
+ if (environmentId) query.set("environmentId", environmentId);
16500
+ if (sandboxId) query.set("sandboxId", sandboxId);
16501
+ if (subjectId) query.set("userId", subjectId);
16502
+ if (options.sessionScope?.trim()) {
16503
+ query.set("sessionScope", options.sessionScope.trim());
16504
+ }
16505
+ if (status !== "all") query.set("status", status);
16506
+ const res = await this.request(
16507
+ `/control/sessions?${query.toString()}`
16508
+ );
16509
+ const items = Array.isArray(res.items) ? res.items : [];
16510
+ return items.map((row) => this.normalizeConversationSession(row));
16511
+ }
16512
+ /**
16513
+ * List active (open) sessions for an environment.
16288
16514
  */
16289
16515
  async listOpenSessions(filters) {
16290
- return this.listSessionsForEnvironment(filters.environmentId, "active");
16516
+ return this.listSessions({ ...filters, status: "active" });
16291
16517
  }
16292
16518
  /**
16293
16519
  * List closed sessions for an environment (conversations that have disconnected).
16294
16520
  */
16295
16521
  async listClosedSessions(filters) {
16296
- return this.listSessionsForEnvironment(filters.environmentId, "closed");
16522
+ return this.listSessions({ ...filters, status: "closed" });
16297
16523
  }
16298
16524
  async getUserEnvironmentState(options) {
16299
16525
  const query = new URLSearchParams({
@@ -16328,14 +16554,6 @@ var Granular = class _Granular {
16328
16554
  });
16329
16555
  return result.readAtBySessionId || {};
16330
16556
  }
16331
- async listSessionsForEnvironment(environmentId, status) {
16332
- const query = new URLSearchParams({ environmentId, status });
16333
- const res = await this.request(
16334
- `/control/sessions?${query.toString()}`
16335
- );
16336
- const items = Array.isArray(res.items) ? res.items : [];
16337
- return items.map((row) => this.normalizeConversationSession(row));
16338
- }
16339
16557
  normalizeConversationSession(row) {
16340
16558
  const sessionId = String(row.sessionId ?? row.session_id ?? "");
16341
16559
  const environmentId = String(row.environmentId ?? row.environment_id ?? "");
@@ -16394,6 +16612,7 @@ var Granular = class _Granular {
16394
16612
  */
16395
16613
  async createSession(options) {
16396
16614
  const clientId = options.clientId || `client_${Date.now()}`;
16615
+ const sessionScope = options.sessionScope?.trim() || void 0;
16397
16616
  await this.activateEnvironment(options.environmentId);
16398
16617
  const envData = await this.environments.get(options.environmentId);
16399
16618
  const environment = this.bindEnvironmentHandle(envData);
@@ -16402,6 +16621,8 @@ var Granular = class _Granular {
16402
16621
  body: JSON.stringify({
16403
16622
  environmentId: options.environmentId,
16404
16623
  clientId,
16624
+ sessionScope,
16625
+ capabilities: sessionScope ? { sessionScope } : void 0,
16405
16626
  initialHeap: options.initialHeap
16406
16627
  })
16407
16628
  });
@@ -19582,6 +19803,7 @@ function buildGranularAgentSystemPrompt(input) {
19582
19803
  - \`groundedObjects.save(...)\` only accepts scalar values, session files, runtime records/sandbox instances, or arrays of runtime records/sandbox instances from one class. Do not save plain action/effect result objects; fetch affected records first or answer from summaries with \`replyToUser(...)\`.
19583
19804
  - Do not use \`showObjects({ entries: [...] })\` or \`showObjects({ saveAs, entries })\`. Save ordered pages, queues, search results, or ranked lists with \`groundedObjects.save(...)\`, then call \`showObjects({ variableNames: [...] })\` once.
19584
19805
  - Use \`showAgentResponse({ reply, show: [record, action] })\` when one assistant message should combine text, grounded records, files, prepared actions, or action suggestions. The \`action\` can be a state handle such as \`record.lifecycle.approved\` or an action handle such as \`record.lifecycle.approved.reach()\`.
19806
+ - Pass grounded records directly in \`show\` when the default record presentation answers the request. When the user asks for particular columns, comparisons, or computed values, import \`table\` (and \`relativeTime\` when useful) from \`@granular/agent\` and call \`showAgentResponse({ reply, show: table(records, [{ label: "Object", value: record => record.label }, { label: "When", value: record => relativeTime(record.timestamp) }]) })\`. Column callbacks must be synchronous and return a scalar, \`Date\`, or \`relativeTime(...)\`; they run inside the job and only resolved cells are persisted.
19585
19807
  - Do not use deprecated side-channel helpers such as \`agent_text_message(...)\`, \`agent_heap_objects(...)\`, or \`agent_message(...)\` unless the generated types expose no Harness v3 helper alternative.
19586
19808
  - When the user asks to show, list, display, open, or "show them" for records you found, call \`showObjects(...)\`; do not answer only with a count or text summary.
19587
19809
  - For count-only questions such as "how many", "how many X do I have", or "what is the total number of X", call the entity \`.count(...)\` or use page \`totalCount\` only when a page is already needed for other reasons. Answer with \`replyToUser(...)\` only. Do not call \`showObjects(...)\`, \`saveAs\`, or \`groundedObjects.save(...)\` unless the user also asked to see records or a later requested action needs a reusable record selection.
@@ -19852,11 +20074,12 @@ ${actionIndex}
19852
20074
  - Use typed plan fields and helpers directly. For visible blocker text, use the declared blocker-formatting helper when available instead of hand-written object casts.
19853
20075
  - A suggestion is a recommendation button: \`await stateHandle.suggest(message)\`. A prepared action is the editable form the user can review/run: \`const action = stateHandle.reach(); const prepared = await action.open()\`.
19854
20076
  - Use suggestions when the user asks "what can I do next?", asks for options, or gives an unclear intent. Prefer 2 to 5 concrete suggestions and keep text short.
19855
- - Open a prepared action when the user asks for one clear action. Prefill only values grounded in the user request, conversation, files, selected records, or fresh reads. Leave unknown fields empty; do not invent them.
20077
+ - Opening a prepared action does not execute the underlying mutation. Use \`.open()\` only when the user asks to prepare, review, show, or edit an action before running it, when a new record must be prepared, or when a reviewable form must collect missing editable inputs. Prefill only grounded values and leave unknown fields empty.
20078
+ - When the user explicitly commands an existing-record mutation such as approve, reject, block, route, send, or update, and one visible domain action uniquely matches, call that domain action directly. Do not substitute a state-handle \`.open()\` artifact for execution. If runtime policy requires confirmation, invoke the action once and let the runtime pause and resume that same invocation.
19856
20079
  - When a grounded related/context record can satisfy the prepared action through declared relationships, use those relationships to fill required relationship inputs before opening or updating the action. If a required relationship remains empty, continue through declared relationship chains from the grounded object when the next hop can fill that slot. Do not only save the context record in memory while leaving derivable relationship slots blank.
19857
20080
  - A derived intermediate relationship is not enough when another required relationship is still reachable from it. For example, if a team gives a cost center and the action also requires a budget, traverse the cost center's declared budget relationship before opening the action.
19858
20081
  - If the user says they have a document/work item/event but no matching record is found, check for a declared class-level new-record state/action handle or importable backend create/preparation action for that named class before giving up. Use grounded required fields to open the prepared action or call the create/preparation action; if required values are still missing and no prepared action can collect them, ask only for those values. Do not claim the record already exists.
19859
- - Do not ask for confirmation before opening a prepared action. The prepared action is itself reviewable. Use \`userInteraction.askConfirmation\` only when the user explicitly asks for yes/no approval, the selected action is ambiguous after grounding, or the domain/runtime asks for confirmation.
20082
+ - Do not ask for confirmation before opening a prepared action requested for review; the artifact is itself reviewable. For a direct mutation command, do not open an artifact merely to obtain confirmation. Use \`userInteraction.askConfirmation\` only when the user explicitly asks for a separate yes/no step, material ambiguity remains after grounding, or policy requires confirmation outside the invoked action runtime.
19860
20083
  - If the action cannot continue because of missing input, stale state, missing relationships, related-state requirements, or permissions, keep/show the prepared action at that blocker and explain the next needed person, record, or value. Do not skip workflow steps or target a later state.
19861
20084
  - Reuse an already-open prepared action for the same target/action when available: update it, show it again, or explain what is still needed instead of creating a duplicate.
19862
20085
  - If an open prepared action needs edits, prefer the returned record helper: \`const prepared = await action.open(); await prepared.updateInputs({ inputValues, relationships });\`. Use \`artifacts.updateInputs(id, patch)\` only when you only have an id.
@@ -20048,8 +20271,9 @@ function resolveHarnessTemplate(templateId = "stable", options) {
20048
20271
  }
20049
20272
 
20050
20273
  // src/openai-usage.ts
20051
- var OPENAI_PRICING_SOURCE_URL = "https://developers.openai.com/api/docs/models/gpt-5.4/";
20052
- var OPENAI_PRICING_EFFECTIVE_DATE = "2026-05-19";
20274
+ var OPENAI_GPT_5_4_PRICING_SOURCE_URL = "https://developers.openai.com/api/docs/models/gpt-5.4/";
20275
+ var OPENAI_GPT_5_6_PRICING_SOURCE_URL = "https://developers.openai.com/api/docs/pricing";
20276
+ var OPENAI_LONG_CONTEXT_THRESHOLD_TOKENS = 272e3;
20053
20277
  var OPENAI_MODEL_PRICING_USD_PER_MILLION = {
20054
20278
  "gpt-5.4": {
20055
20279
  provider: "openai",
@@ -20058,8 +20282,26 @@ var OPENAI_MODEL_PRICING_USD_PER_MILLION = {
20058
20282
  inputUsdPerMillion: 2.5,
20059
20283
  cachedInputUsdPerMillion: 0.25,
20060
20284
  outputUsdPerMillion: 15,
20061
- sourceUrl: OPENAI_PRICING_SOURCE_URL,
20062
- effectiveDate: OPENAI_PRICING_EFFECTIVE_DATE
20285
+ sourceUrl: OPENAI_GPT_5_4_PRICING_SOURCE_URL,
20286
+ effectiveDate: "2026-05-19"
20287
+ },
20288
+ "gpt-5.6-luna": {
20289
+ provider: "openai",
20290
+ model: "gpt-5.6-luna",
20291
+ currency: "USD",
20292
+ inputUsdPerMillion: 1,
20293
+ cachedInputUsdPerMillion: 0.1,
20294
+ cacheWriteUsdPerMillion: 1.25,
20295
+ outputUsdPerMillion: 6,
20296
+ sourceUrl: OPENAI_GPT_5_6_PRICING_SOURCE_URL,
20297
+ effectiveDate: "2026-07-11",
20298
+ longContextThresholdTokens: OPENAI_LONG_CONTEXT_THRESHOLD_TOKENS,
20299
+ longContextPricing: {
20300
+ inputUsdPerMillion: 2,
20301
+ cachedInputUsdPerMillion: 0.2,
20302
+ cacheWriteUsdPerMillion: 2.5,
20303
+ outputUsdPerMillion: 9
20304
+ }
20063
20305
  }
20064
20306
  };
20065
20307
  function asRecord5(value) {
@@ -20072,8 +20314,18 @@ function numberField(record, key) {
20072
20314
  function microsPerMillion(usdPerMillion) {
20073
20315
  return Math.round(usdPerMillion * 1e6);
20074
20316
  }
20075
- function getOpenAIModelPricing(model) {
20076
- return OPENAI_MODEL_PRICING_USD_PER_MILLION[model] || null;
20317
+ function getOpenAIModelPricing(model, inputTokens = 0) {
20318
+ const pricing = OPENAI_MODEL_PRICING_USD_PER_MILLION[model];
20319
+ if (!pricing) return null;
20320
+ const threshold = pricing.longContextThresholdTokens ?? null;
20321
+ if (pricing.longContextPricing && typeof threshold === "number" && inputTokens > threshold) {
20322
+ return {
20323
+ ...pricing,
20324
+ ...pricing.longContextPricing,
20325
+ contextTier: "long"
20326
+ };
20327
+ }
20328
+ return { ...pricing, contextTier: "short" };
20077
20329
  }
20078
20330
  function normalizeOpenAIUsage(rawUsage) {
20079
20331
  const usage = asRecord5(rawUsage);
@@ -20081,6 +20333,7 @@ function normalizeOpenAIUsage(rawUsage) {
20081
20333
  return {
20082
20334
  inputTokens: 0,
20083
20335
  cachedInputTokens: 0,
20336
+ cacheWriteTokens: 0,
20084
20337
  uncachedInputTokens: 0,
20085
20338
  outputTokens: 0,
20086
20339
  reasoningTokens: 0,
@@ -20096,20 +20349,28 @@ function normalizeOpenAIUsage(rawUsage) {
20096
20349
  inputTokens,
20097
20350
  numberField(inputDetails, "cached_tokens") || numberField(inputDetails, "cached_input_tokens")
20098
20351
  );
20352
+ const cacheWriteTokens = Math.min(
20353
+ Math.max(inputTokens - cachedInputTokens, 0),
20354
+ numberField(inputDetails, "cache_write_tokens")
20355
+ );
20099
20356
  const reasoningTokens = numberField(outputDetails, "reasoning_tokens") || numberField(outputDetails, "reasoning_output_tokens");
20100
20357
  return {
20101
20358
  inputTokens,
20102
20359
  cachedInputTokens,
20103
- uncachedInputTokens: Math.max(inputTokens - cachedInputTokens, 0),
20360
+ cacheWriteTokens,
20361
+ uncachedInputTokens: Math.max(
20362
+ inputTokens - cachedInputTokens - cacheWriteTokens,
20363
+ 0
20364
+ ),
20104
20365
  outputTokens,
20105
20366
  reasoningTokens,
20106
20367
  totalTokens
20107
20368
  };
20108
20369
  }
20109
20370
  function calculateOpenAITokenSpend(model, rawUsage) {
20110
- const pricing = getOpenAIModelPricing(model);
20111
- if (!pricing) return null;
20112
20371
  const usage = normalizeOpenAIUsage(rawUsage);
20372
+ const pricing = getOpenAIModelPricing(model, usage.inputTokens);
20373
+ if (!pricing) return null;
20113
20374
  const inputPricePerMillionMicros = microsPerMillion(
20114
20375
  pricing.inputUsdPerMillion
20115
20376
  );
@@ -20119,14 +20380,19 @@ function calculateOpenAITokenSpend(model, rawUsage) {
20119
20380
  const outputPricePerMillionMicros = microsPerMillion(
20120
20381
  pricing.outputUsdPerMillion
20121
20382
  );
20383
+ const cacheWritePricePerMillionMicros = typeof pricing.cacheWriteUsdPerMillion === "number" ? microsPerMillion(pricing.cacheWriteUsdPerMillion) : null;
20384
+ const cacheWriteCostMicros = Math.round(
20385
+ usage.cacheWriteTokens * (cacheWritePricePerMillionMicros ?? inputPricePerMillionMicros) / 1e6
20386
+ );
20122
20387
  const amountMicros = Math.round(
20123
- (usage.uncachedInputTokens * inputPricePerMillionMicros + usage.cachedInputTokens * cachedInputPricePerMillionMicros + usage.outputTokens * outputPricePerMillionMicros) / 1e6
20388
+ (usage.uncachedInputTokens * inputPricePerMillionMicros + usage.cachedInputTokens * cachedInputPricePerMillionMicros + usage.cacheWriteTokens * (cacheWritePricePerMillionMicros ?? inputPricePerMillionMicros) + usage.outputTokens * outputPricePerMillionMicros) / 1e6
20124
20389
  );
20125
20390
  return {
20126
20391
  provider: "openai",
20127
20392
  model,
20128
20393
  inputTokens: usage.inputTokens,
20129
20394
  cachedInputTokens: usage.cachedInputTokens,
20395
+ cacheWriteTokens: usage.cacheWriteTokens,
20130
20396
  uncachedInputTokens: usage.uncachedInputTokens,
20131
20397
  outputTokens: usage.outputTokens,
20132
20398
  reasoningTokens: usage.reasoningTokens,
@@ -20135,7 +20401,11 @@ function calculateOpenAITokenSpend(model, rawUsage) {
20135
20401
  currency: "USD",
20136
20402
  inputPricePerMillionMicros,
20137
20403
  cachedInputPricePerMillionMicros,
20404
+ cacheWritePricePerMillionMicros,
20405
+ cacheWriteCostMicros,
20138
20406
  outputPricePerMillionMicros,
20407
+ pricingContextTier: pricing.contextTier || "short",
20408
+ longContextThresholdTokens: pricing.longContextThresholdTokens ?? null,
20139
20409
  pricingSource: pricing.sourceUrl,
20140
20410
  pricingEffectiveAt: pricing.effectiveDate,
20141
20411
  usage