@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.
package/dist/index.mjs CHANGED
@@ -22,11 +22,11 @@ var __export = (target, all) => {
22
22
  for (var name in all)
23
23
  __defProp(target, name, { get: all[name], enumerable: true });
24
24
  };
25
- var __copyProps = (to, from, except, desc) => {
26
- if (from && typeof from === "object" || typeof from === "function") {
27
- for (let key of __getOwnPropNames(from))
25
+ var __copyProps = (to, from2, except, desc) => {
26
+ if (from2 && typeof from2 === "object" || typeof from2 === "function") {
27
+ for (let key of __getOwnPropNames(from2))
28
28
  if (!__hasOwnProp.call(to, key) && key !== except)
29
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
29
+ __defProp(to, key, { get: () => from2[key], enumerable: !(desc = __getOwnPropDesc(from2, key)) || desc.enumerable });
30
30
  }
31
31
  return to;
32
32
  };
@@ -4043,16 +4043,37 @@ var WSClient = class {
4043
4043
  tokenRefreshTimer = null;
4044
4044
  isExplicitlyDisconnected = false;
4045
4045
  reconnectAttempts = 0;
4046
+ connectPromise = null;
4047
+ connectionEpoch = 0;
4048
+ cancelConnectAttempt = null;
4046
4049
  options;
4047
4050
  constructor(options) {
4048
4051
  this.options = options;
4049
4052
  this.url = options.url;
4050
4053
  this.sessionId = options.sessionId;
4051
4054
  this.token = options.token;
4055
+ if (options.initialDocumentSnapshot) {
4056
+ this.seedDocumentSnapshot(options.initialDocumentSnapshot);
4057
+ }
4052
4058
  }
4053
4059
  get currentSessionId() {
4054
4060
  return this.sessionId;
4055
4061
  }
4062
+ seedDocumentSnapshot(document) {
4063
+ if (!document || typeof document !== "object" || Array.isArray(document)) {
4064
+ return;
4065
+ }
4066
+ try {
4067
+ this.doc = document instanceof Uint8Array ? Automerge.load(document) : Automerge.from(document);
4068
+ this.syncState = Automerge.initSyncState();
4069
+ this.emit("sync", this.doc);
4070
+ } catch (error) {
4071
+ console.warn("[Granular] Failed to seed cached session document", error);
4072
+ }
4073
+ }
4074
+ saveDocumentSnapshot() {
4075
+ return Automerge.save(this.doc);
4076
+ }
4056
4077
  clearTokenRefreshTimer() {
4057
4078
  if (this.tokenRefreshTimer) {
4058
4079
  clearTimeout(this.tokenRefreshTimer);
@@ -4162,8 +4183,23 @@ var WSClient = class {
4162
4183
  * Connect to the WebSocket server
4163
4184
  * @returns {Promise<void>} Resolves when connection is open
4164
4185
  */
4165
- async connect() {
4186
+ async connect(options = {}) {
4187
+ if (this.ws?.readyState === READY_STATE_OPEN) return;
4188
+ if (this.connectPromise) return this.connectPromise;
4189
+ const connectPromise = this.connectAttempt(options.signal);
4190
+ this.connectPromise = connectPromise;
4191
+ try {
4192
+ await connectPromise;
4193
+ } finally {
4194
+ if (this.connectPromise === connectPromise) {
4195
+ this.connectPromise = null;
4196
+ }
4197
+ }
4198
+ }
4199
+ async connectAttempt(signal) {
4200
+ if (signal?.aborted) throw new Error("WebSocket connect aborted");
4166
4201
  const token = await this.resolveTokenForConnect();
4202
+ if (signal?.aborted) throw new Error("WebSocket connect aborted");
4167
4203
  this.isExplicitlyDisconnected = false;
4168
4204
  this.scheduleTokenRefresh();
4169
4205
  if (this.reconnectTimer) {
@@ -4175,7 +4211,7 @@ var WSClient = class {
4175
4211
  try {
4176
4212
  const wsModule = await Promise.resolve().then(() => (init_wrapper(), wrapper_exports));
4177
4213
  WebSocketClass = wsModule.default || wsModule;
4178
- } catch (e) {
4214
+ } catch {
4179
4215
  }
4180
4216
  }
4181
4217
  if (!WebSocketClass) {
@@ -4183,83 +4219,97 @@ var WSClient = class {
4183
4219
  'No WebSocket implementation found. If using Node.js, please install "ws" and pass the constructor to the SDK options: { WebSocketCtor: WebSocket }.'
4184
4220
  );
4185
4221
  }
4222
+ const epoch = ++this.connectionEpoch;
4223
+ const wsUrl = new URL(this.url);
4224
+ wsUrl.searchParams.set("sessionId", this.sessionId);
4225
+ wsUrl.searchParams.set("token", token);
4226
+ const socket = new WebSocketClass(wsUrl.toString());
4227
+ this.ws = socket;
4186
4228
  return new Promise((resolve, reject) => {
4187
- try {
4188
- const wsUrl = new URL(this.url);
4189
- wsUrl.searchParams.set("sessionId", this.sessionId);
4190
- wsUrl.searchParams.set("token", token);
4191
- this.ws = new WebSocketClass(wsUrl.toString());
4192
- if (!this.ws) throw new Error("Failed to create WebSocket");
4193
- const socket = this.ws;
4194
- if (typeof socket.on === "function") {
4195
- socket.on("open", () => {
4196
- if (this.reconnectTimer) {
4197
- clearTimeout(this.reconnectTimer);
4198
- this.reconnectTimer = null;
4199
- }
4200
- this.reconnectAttempts = 0;
4201
- this.emit("open", {});
4202
- resolve();
4203
- });
4204
- socket.on("message", (data) => {
4205
- try {
4206
- const message = JSON.parse(data.toString());
4207
- this.handleMessage(message);
4208
- } catch (error) {
4209
- console.error("[Granular] Failed to parse message:", error);
4210
- }
4211
- });
4212
- socket.on("error", (error) => {
4213
- this.emit("error", error);
4214
- if (socket.readyState !== READY_STATE_OPEN) {
4215
- reject(error);
4216
- }
4217
- });
4218
- socket.on("close", (code, reason) => {
4219
- this.handleDisconnect({
4220
- code,
4221
- reason: this.normalizeReason(reason),
4222
- // ws does not provide wasClean on Node-style close callback
4223
- wasClean: code === 1e3
4224
- });
4225
- });
4229
+ let settled = false;
4230
+ const isCurrent = () => this.connectionEpoch === epoch && this.ws === socket;
4231
+ const finish = (error) => {
4232
+ if (settled) return;
4233
+ settled = true;
4234
+ if (this.cancelConnectAttempt === handleAbort) {
4235
+ this.cancelConnectAttempt = null;
4236
+ }
4237
+ signal?.removeEventListener("abort", handleAbort);
4238
+ if (error) {
4239
+ reject(error instanceof Error ? error : new Error(String(error)));
4226
4240
  } else {
4227
- this.ws.onopen = () => {
4228
- if (this.reconnectTimer) {
4229
- clearTimeout(this.reconnectTimer);
4230
- this.reconnectTimer = null;
4231
- }
4232
- this.reconnectAttempts = 0;
4233
- this.emit("open", {});
4234
- resolve();
4235
- };
4236
- this.ws.onmessage = (event) => {
4237
- try {
4238
- const data = event.data;
4239
- const message = JSON.parse(data.toString());
4240
- this.handleMessage(message);
4241
- } catch (error) {
4242
- console.error("[Granular] Failed to parse message:", error);
4243
- }
4244
- };
4245
- this.ws.onerror = (event) => {
4246
- const error = new Error("WebSocket error");
4247
- error.event = event;
4248
- this.emit("error", error);
4249
- if (this.ws?.readyState !== READY_STATE_OPEN) {
4250
- reject(error);
4251
- }
4252
- };
4253
- this.ws.onclose = (event) => {
4254
- this.handleDisconnect({
4255
- code: event.code,
4256
- reason: event.reason,
4257
- wasClean: event.wasClean
4258
- });
4259
- };
4241
+ resolve();
4260
4242
  }
4261
- } catch (error) {
4262
- reject(error);
4243
+ };
4244
+ const closeStaleSocket = () => {
4245
+ try {
4246
+ socket.close(1e3, "Stale connection attempt");
4247
+ } catch {
4248
+ }
4249
+ };
4250
+ const handleAbort = () => {
4251
+ if (isCurrent()) {
4252
+ this.connectionEpoch += 1;
4253
+ this.ws = null;
4254
+ }
4255
+ closeStaleSocket();
4256
+ finish(new Error("WebSocket connect aborted"));
4257
+ };
4258
+ this.cancelConnectAttempt = handleAbort;
4259
+ const handleOpen = () => {
4260
+ if (!isCurrent()) {
4261
+ closeStaleSocket();
4262
+ return;
4263
+ }
4264
+ this.reconnectAttempts = 0;
4265
+ this.emit("open", {});
4266
+ finish();
4267
+ };
4268
+ const handleMessage = (data) => {
4269
+ if (!isCurrent()) return;
4270
+ try {
4271
+ const text = typeof data === "string" ? data : data && typeof data === "object" && "toString" in data ? String(data.toString()) : "";
4272
+ this.handleMessage(JSON.parse(text));
4273
+ } catch (error) {
4274
+ console.error("[Granular] Failed to parse message:", error);
4275
+ }
4276
+ };
4277
+ const handleError = (error) => {
4278
+ if (!isCurrent()) return;
4279
+ const typedError = error instanceof Error ? error : new Error("WebSocket error");
4280
+ this.emit("error", typedError);
4281
+ if (socket.readyState !== READY_STATE_OPEN) finish(typedError);
4282
+ };
4283
+ const handleClose = (close) => {
4284
+ if (!isCurrent()) return;
4285
+ if (!settled) {
4286
+ finish(
4287
+ new Error(
4288
+ `WebSocket closed before ready${close.code ? ` (code=${close.code})` : ""}`
4289
+ )
4290
+ );
4291
+ }
4292
+ this.handleDisconnect({
4293
+ code: close.code,
4294
+ reason: this.normalizeReason(close.reason),
4295
+ wasClean: close.wasClean
4296
+ });
4297
+ };
4298
+ signal?.addEventListener("abort", handleAbort, { once: true });
4299
+ const nodeSocket = socket;
4300
+ if (typeof nodeSocket.on === "function") {
4301
+ nodeSocket.on("open", handleOpen);
4302
+ nodeSocket.on("message", handleMessage);
4303
+ nodeSocket.on("error", handleError);
4304
+ nodeSocket.on(
4305
+ "close",
4306
+ (code, reason) => handleClose({ code, reason, wasClean: code === 1e3 })
4307
+ );
4308
+ } else {
4309
+ socket.onopen = handleOpen;
4310
+ socket.onmessage = (event) => handleMessage(event.data);
4311
+ socket.onerror = handleError;
4312
+ socket.onclose = (event) => handleClose(event);
4263
4313
  }
4264
4314
  });
4265
4315
  }
@@ -4277,9 +4327,58 @@ var WSClient = class {
4277
4327
  return void 0;
4278
4328
  }
4279
4329
  rejectPending(error) {
4280
- this.messageQueue.forEach((pending) => pending.reject(error));
4330
+ this.messageQueue.forEach((pending) => {
4331
+ clearTimeout(pending.timeout);
4332
+ pending.reject(error);
4333
+ });
4281
4334
  this.messageQueue = [];
4282
4335
  }
4336
+ emitReconnectErrorMessage(error) {
4337
+ const reconnectInfo = {
4338
+ error,
4339
+ sessionId: this.sessionId,
4340
+ timestamp: Date.now()
4341
+ };
4342
+ this.emit("reconnect_error", reconnectInfo);
4343
+ if (this.options.onReconnectError) {
4344
+ try {
4345
+ this.options.onReconnectError(reconnectInfo);
4346
+ } catch (callbackError) {
4347
+ console.error(
4348
+ "[Granular] onReconnectError callback failed:",
4349
+ callbackError
4350
+ );
4351
+ }
4352
+ }
4353
+ }
4354
+ scheduleReconnectAttempt() {
4355
+ if (this.isExplicitlyDisconnected || this.reconnectTimer) return null;
4356
+ const baseReconnectDelayMs = typeof this.options.reconnectDelayMs === "number" && Number.isFinite(this.options.reconnectDelayMs) && this.options.reconnectDelayMs > 0 ? this.options.reconnectDelayMs : DEFAULT_RECONNECT_DELAY_MS;
4357
+ 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;
4358
+ if (this.reconnectAttempts >= maxReconnectAttempts) {
4359
+ this.emitReconnectErrorMessage(
4360
+ `WebSocket reconnect attempts exhausted after ${maxReconnectAttempts} attempt(s).`
4361
+ );
4362
+ return null;
4363
+ }
4364
+ this.reconnectAttempts += 1;
4365
+ const reconnectDelayMs = Math.min(
4366
+ 3e4,
4367
+ baseReconnectDelayMs * 2 ** Math.max(0, this.reconnectAttempts - 1)
4368
+ );
4369
+ this.reconnectTimer = setTimeout(() => {
4370
+ this.reconnectTimer = null;
4371
+ console.log("[Granular] Attempting reconnect...");
4372
+ this.connect().catch((error) => {
4373
+ console.error("[Granular] Reconnect failed:", error);
4374
+ this.emitReconnectErrorMessage(
4375
+ error instanceof Error ? error.message : String(error)
4376
+ );
4377
+ this.scheduleReconnectAttempt();
4378
+ });
4379
+ }, reconnectDelayMs);
4380
+ return reconnectDelayMs;
4381
+ }
4283
4382
  buildDisconnectError(info) {
4284
4383
  const details = [
4285
4384
  info.code !== void 0 ? `code=${info.code}` : void 0,
@@ -4289,8 +4388,6 @@ var WSClient = class {
4289
4388
  return new Error(`WebSocket disconnected${suffix}`);
4290
4389
  }
4291
4390
  handleDisconnect(close = {}) {
4292
- const baseReconnectDelayMs = typeof this.options.reconnectDelayMs === "number" && Number.isFinite(this.options.reconnectDelayMs) && this.options.reconnectDelayMs > 0 ? this.options.reconnectDelayMs : DEFAULT_RECONNECT_DELAY_MS;
4293
- 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;
4294
4391
  const unexpected = !this.isExplicitlyDisconnected;
4295
4392
  const info = {
4296
4393
  code: close.code,
@@ -4310,32 +4407,9 @@ var WSClient = class {
4310
4407
  const disconnectError = this.buildDisconnectError(info);
4311
4408
  this.rejectPending(disconnectError);
4312
4409
  this.emit("disconnect", info);
4313
- if (this.reconnectAttempts >= maxReconnectAttempts) {
4314
- const reconnectInfo = {
4315
- error: `WebSocket reconnect attempts exhausted after ${maxReconnectAttempts} attempt(s).`,
4316
- sessionId: this.sessionId,
4317
- timestamp: Date.now()
4318
- };
4319
- this.emit("reconnect_error", reconnectInfo);
4320
- if (this.options.onReconnectError) {
4321
- try {
4322
- this.options.onReconnectError(reconnectInfo);
4323
- } catch (callbackError) {
4324
- console.error(
4325
- "[Granular] onReconnectError callback failed:",
4326
- callbackError
4327
- );
4328
- }
4329
- }
4330
- return;
4331
- }
4332
- this.reconnectAttempts += 1;
4333
- const reconnectDelayMs = Math.min(
4334
- 3e4,
4335
- baseReconnectDelayMs * 2 ** Math.max(0, this.reconnectAttempts - 1)
4336
- );
4337
- info.reconnectScheduled = true;
4338
- info.reconnectDelayMs = reconnectDelayMs;
4410
+ const reconnectDelayMs = this.scheduleReconnectAttempt();
4411
+ info.reconnectScheduled = reconnectDelayMs !== null;
4412
+ if (reconnectDelayMs !== null) info.reconnectDelayMs = reconnectDelayMs;
4339
4413
  if (this.options.onUnexpectedClose) {
4340
4414
  try {
4341
4415
  this.options.onUnexpectedClose(info);
@@ -4346,28 +4420,6 @@ var WSClient = class {
4346
4420
  );
4347
4421
  }
4348
4422
  }
4349
- this.reconnectTimer = setTimeout(() => {
4350
- console.log("[Granular] Attempting reconnect...");
4351
- this.connect().catch((error) => {
4352
- console.error("[Granular] Reconnect failed:", error);
4353
- const reconnectInfo = {
4354
- error: error instanceof Error ? error.message : String(error),
4355
- sessionId: this.sessionId,
4356
- timestamp: Date.now()
4357
- };
4358
- this.emit("reconnect_error", reconnectInfo);
4359
- if (this.options.onReconnectError) {
4360
- try {
4361
- this.options.onReconnectError(reconnectInfo);
4362
- } catch (callbackError) {
4363
- console.error(
4364
- "[Granular] onReconnectError callback failed:",
4365
- callbackError
4366
- );
4367
- }
4368
- }
4369
- });
4370
- }, reconnectDelayMs);
4371
4423
  }
4372
4424
  }
4373
4425
  handleMessage(message) {
@@ -4478,6 +4530,7 @@ var WSClient = class {
4478
4530
  const response = message;
4479
4531
  const pending = this.messageQueue.find((q) => q.id === response.id);
4480
4532
  if (pending) {
4533
+ clearTimeout(pending.timeout);
4481
4534
  if (response.type === "rpc_error") {
4482
4535
  pending.reject(
4483
4536
  new Error(
@@ -4523,16 +4576,22 @@ var WSClient = class {
4523
4576
  id
4524
4577
  };
4525
4578
  return new Promise((resolve, reject) => {
4526
- this.messageQueue.push({ resolve, reject, id });
4527
- this.ws.send(JSON.stringify(request));
4528
4579
  const timeoutMs = rpcTimeoutMsForMethod(method);
4529
- setTimeout(() => {
4580
+ const timeout = setTimeout(() => {
4530
4581
  const pending = this.messageQueue.find((q) => q.id === id);
4531
4582
  if (pending) {
4532
4583
  this.messageQueue = this.messageQueue.filter((q) => q.id !== id);
4533
4584
  reject(new Error(`RPC timeout: ${method}`));
4534
4585
  }
4535
4586
  }, timeoutMs);
4587
+ this.messageQueue.push({ resolve, reject, id, timeout });
4588
+ try {
4589
+ this.ws.send(JSON.stringify(request));
4590
+ } catch (error) {
4591
+ clearTimeout(timeout);
4592
+ this.messageQueue = this.messageQueue.filter((q) => q.id !== id);
4593
+ reject(error instanceof Error ? error : new Error(String(error)));
4594
+ }
4536
4595
  });
4537
4596
  }
4538
4597
  async handleIncomingRpc(request) {
@@ -4618,15 +4677,18 @@ var WSClient = class {
4618
4677
  /**
4619
4678
  * Disconnect the WebSocket and clear state
4620
4679
  */
4621
- disconnect() {
4680
+ disconnect(options = {}) {
4622
4681
  this.isExplicitlyDisconnected = true;
4682
+ this.cancelConnectAttempt?.();
4683
+ this.cancelConnectAttempt = null;
4684
+ this.connectionEpoch += 1;
4623
4685
  if (this.reconnectTimer) {
4624
4686
  clearTimeout(this.reconnectTimer);
4625
4687
  this.reconnectTimer = null;
4626
4688
  }
4627
4689
  this.clearTokenRefreshTimer();
4628
4690
  if (this.ws) {
4629
- this.ws.close(1e3, "Client disconnect");
4691
+ this.ws.close(1e3, options.reason || "Client disconnect");
4630
4692
  this.ws = null;
4631
4693
  }
4632
4694
  this.rejectPending(new Error("Client explicitly disconnected"));
@@ -4722,8 +4784,12 @@ function normalizePrompt(rawValue) {
4722
4784
  const source = promptRecord || raw;
4723
4785
  const id = typeof source.id === "string" ? source.id : typeof raw.id === "string" ? raw.id : typeof raw.promptId === "string" ? raw.promptId : "";
4724
4786
  if (!id) return null;
4787
+ const jobId = typeof source.jobId === "string" && source.jobId.trim() ? source.jobId.trim() : typeof raw.jobId === "string" && raw.jobId.trim() ? raw.jobId.trim() : void 0;
4788
+ const turnId = typeof source.turnId === "string" && source.turnId.trim() ? source.turnId.trim() : typeof raw.turnId === "string" && raw.turnId.trim() ? raw.turnId.trim() : void 0;
4725
4789
  return {
4726
4790
  id,
4791
+ ...jobId ? { jobId } : {},
4792
+ ...turnId ? { turnId } : {},
4727
4793
  type: normalizePromptType(source === raw ? raw : { ...raw, ...source }),
4728
4794
  title: typeof source.title === "string" ? source.title : "Input required",
4729
4795
  message: typeof source.message === "string" ? source.message : "",
@@ -4810,6 +4876,9 @@ var Session = class {
4810
4876
  this.initialQuota = options.initialQuota || null;
4811
4877
  this.setupEventHandlers();
4812
4878
  this.setupToolInvokeHandler();
4879
+ this.currentDomainRevision = this.extractDomainRevisionFromDoc(
4880
+ this.client.doc
4881
+ );
4813
4882
  }
4814
4883
  extractDomainRevisionFromDoc(doc) {
4815
4884
  const domain = doc?.domain;
@@ -5715,6 +5784,7 @@ function normalizeJobAgentMessageEnvelope(data) {
5715
5784
  }
5716
5785
  return {
5717
5786
  jobId: d.jobId,
5787
+ ...typeof d.turnId === "string" && d.turnId.trim() ? { turnId: d.turnId.trim() } : {},
5718
5788
  message: {
5719
5789
  messageId: d.messageId,
5720
5790
  kind: d.kind === "artifacts" ? "artifacts" : "text",
@@ -6373,9 +6443,14 @@ function normalizeShowRefs(value) {
6373
6443
  variableNames: normalizeRefs(record.variableNames),
6374
6444
  fileIds: normalizeRefs(record.fileIds),
6375
6445
  sessionArtifactIds: normalizeRefs(record.sessionArtifactIds),
6376
- actionSuggestions: normalizeActionSuggestions(record.actionSuggestions)
6446
+ actionSuggestions: normalizeActionSuggestions(record.actionSuggestions),
6447
+ tables: Array.isArray(record.tables) ? record.tables.filter(
6448
+ (table) => Boolean(
6449
+ table && typeof table === "object" && !Array.isArray(table) && Array.isArray(table.columns) && Array.isArray(table.rows)
6450
+ )
6451
+ ) : void 0
6377
6452
  };
6378
- return show.entryPaths || show.listNames || show.variableNames || show.fileIds || show.sessionArtifactIds || show.actionSuggestions ? show : void 0;
6453
+ return show.entryPaths || show.listNames || show.variableNames || show.fileIds || show.sessionArtifactIds || show.actionSuggestions || show.tables ? show : void 0;
6379
6454
  }
6380
6455
  function normalizeActionSuggestions(value) {
6381
6456
  if (!Array.isArray(value)) return void 0;
@@ -6397,6 +6472,76 @@ function normalizeActionSuggestions(value) {
6397
6472
  }
6398
6473
  return suggestions.length ? suggestions : void 0;
6399
6474
  }
6475
+ var TRANSCRIPT_MESSAGE_PART_LIMIT = 128;
6476
+ var TRANSCRIPT_MESSAGE_PART_TEXT_LIMIT = 2e5;
6477
+ var TRANSCRIPT_MESSAGE_PART_ACTION_LABEL_LIMIT = 1e3;
6478
+ var TRANSCRIPT_MESSAGE_ACTION_LIMIT = 64;
6479
+ function normalizeConversationMessageActions(value) {
6480
+ if (!Array.isArray(value) || value.length === 0) return void 0;
6481
+ const actions = [];
6482
+ for (const item of value.slice(0, TRANSCRIPT_MESSAGE_ACTION_LIMIT)) {
6483
+ const record = asRecord3(item);
6484
+ const kind = record?.kind;
6485
+ const label = trimString(record?.label ?? record?.title);
6486
+ const status = record?.status;
6487
+ if (kind !== "frontend" && kind !== "backend" && kind !== "system" || !label || label.length > TRANSCRIPT_MESSAGE_PART_ACTION_LABEL_LIMIT) {
6488
+ continue;
6489
+ }
6490
+ actions.push({
6491
+ kind,
6492
+ label,
6493
+ ...status === "done" || status === "queued" || status === "failed" ? { status } : {}
6494
+ });
6495
+ }
6496
+ return actions.length ? actions : void 0;
6497
+ }
6498
+ function normalizeConversationMessageParts(value, canonicalContent, canonicalActions) {
6499
+ if (!Array.isArray(value) || value.length === 0 || value.length > TRANSCRIPT_MESSAGE_PART_LIMIT) {
6500
+ return void 0;
6501
+ }
6502
+ const parts = [];
6503
+ const canonicalActionsById = new Map(
6504
+ (canonicalActions || []).map((action) => [
6505
+ `${action.kind}:${action.label}`,
6506
+ action
6507
+ ])
6508
+ );
6509
+ const seenActionIds = /* @__PURE__ */ new Set();
6510
+ let textLength = 0;
6511
+ for (const item of value) {
6512
+ const record = asRecord3(item);
6513
+ if (!record) return void 0;
6514
+ if (record.type === "text") {
6515
+ if (typeof record.text !== "string" || record.text.length === 0) {
6516
+ return void 0;
6517
+ }
6518
+ textLength += record.text.length;
6519
+ if (textLength > TRANSCRIPT_MESSAGE_PART_TEXT_LIMIT) return void 0;
6520
+ parts.push({ type: "text", text: record.text });
6521
+ continue;
6522
+ }
6523
+ if (record.type !== "action") return void 0;
6524
+ const action = asRecord3(record.action);
6525
+ const kind = action?.kind;
6526
+ const label = trimString(action?.label);
6527
+ if (kind !== "frontend" && kind !== "backend" && kind !== "system" || !label || label.length > TRANSCRIPT_MESSAGE_PART_ACTION_LABEL_LIMIT) {
6528
+ return void 0;
6529
+ }
6530
+ const actionId = `${kind}:${label}`;
6531
+ const canonicalAction = canonicalActionsById.get(actionId);
6532
+ if (!canonicalAction) return void 0;
6533
+ if (seenActionIds.has(actionId)) continue;
6534
+ seenActionIds.add(actionId);
6535
+ parts.push({
6536
+ type: "action",
6537
+ action: canonicalAction
6538
+ });
6539
+ }
6540
+ const orderedText = parts.filter(
6541
+ (part) => part.type === "text"
6542
+ ).map((part) => part.text).join("");
6543
+ return orderedText === canonicalContent ? parts : void 0;
6544
+ }
6400
6545
  function stringifyTranscriptValue(value, fallback = "") {
6401
6546
  if (typeof value === "string") {
6402
6547
  return value.trim() || fallback;
@@ -6555,10 +6700,12 @@ function normalizeConversationMessage(raw, artifactsById) {
6555
6700
  const content = trimString(
6556
6701
  record.content ?? record.reply ?? record.message ?? record.text
6557
6702
  );
6703
+ const actions = role === "assistant" ? normalizeConversationMessageActions(record.actions) : void 0;
6704
+ const parts = role === "assistant" ? normalizeConversationMessageParts(record.parts, content, actions) : void 0;
6558
6705
  const show = normalizeShowRefs(record.show);
6559
6706
  const id = asString(record.id) || crypto.randomUUID();
6560
6707
  const timestamp = asNumber(record.timestamp) || asNumber(record.ts) || 0;
6561
- if (!content && !show) return null;
6708
+ if (!content && !show && !actions?.length) return null;
6562
6709
  const artifactHistory = buildArtifactHistory(show, artifactsById);
6563
6710
  const historyContent = role === "assistant" ? content && artifactHistory ? `[Assistant reply]
6564
6711
  ${content}
@@ -6573,6 +6720,8 @@ ${content}` : artifactHistory : void 0;
6573
6720
  jobId: asString(record.jobId),
6574
6721
  promptId: asString(record.promptId),
6575
6722
  show,
6723
+ actions,
6724
+ parts,
6576
6725
  historyContent,
6577
6726
  source: "conversation"
6578
6727
  };
@@ -12150,7 +12299,12 @@ async function recordOpenAIUsageSpend(options) {
12150
12299
  const metadata = {
12151
12300
  ...options.metadata || {},
12152
12301
  ...options.usage.rawUsage !== void 0 ? { openaiUsage: options.usage.rawUsage } : {},
12153
- usageContext: context
12302
+ usageContext: context,
12303
+ pricingContextTier: options.usage.pricingContextTier,
12304
+ cacheWritePricePerMillionMicros: options.usage.cacheWritePricePerMillionMicros,
12305
+ cacheWriteTokens: options.usage.cacheWriteTokens,
12306
+ cacheWriteCostMicros: options.usage.cacheWriteCostMicros,
12307
+ longContextThresholdTokens: options.usage.longContextThresholdTokens
12154
12308
  };
12155
12309
  const response = await fetch(
12156
12310
  `${toGranularHttpBase(options.apiUrl)}/control/spend/events`,
@@ -13078,11 +13232,20 @@ function buildStateMachineModelMutations(modelPath, machines) {
13078
13232
  return mutations;
13079
13233
  }
13080
13234
  function buildMachineTypes(classSummary, machine) {
13235
+ const stateGlossary = machine.states.map((state) => {
13236
+ const label = state.label && state.label !== state.name ? state.label : null;
13237
+ const meaning = [label, state.description].filter(Boolean).join(" \u2014 ");
13238
+ const finalMarker = state.isFinal ? " Final state." : "";
13239
+ return `${state.name}${meaning ? `: ${meaning}` : "."}${finalMarker}`;
13240
+ });
13081
13241
  return [
13082
13242
  {
13083
13243
  kind: "union",
13084
13244
  name: stateTypeName(classSummary.name, machine.name),
13085
- docs: [`Allowed states for ${classSummary.name}.${machine.name}.`],
13245
+ docs: [
13246
+ `Allowed states for ${classSummary.name}.${machine.name}.`,
13247
+ ...stateGlossary
13248
+ ],
13086
13249
  members: machine.states.map((state) => state.name)
13087
13250
  },
13088
13251
  {
@@ -13430,7 +13593,7 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
13430
13593
  },
13431
13594
  add_transition: async (value, {
13432
13595
  name,
13433
- from,
13596
+ from: from2,
13434
13597
  to,
13435
13598
  label,
13436
13599
  description,
@@ -13445,7 +13608,7 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
13445
13608
  value.target.add_state_machine_transition(
13446
13609
  value.name,
13447
13610
  name,
13448
- from,
13611
+ from2,
13449
13612
  to,
13450
13613
  {
13451
13614
  label,
@@ -13895,6 +14058,18 @@ function buildEffectMetamodelMutations(toolPath, spec) {
13895
14058
  }
13896
14059
 
13897
14060
  // src/client.ts
14061
+ var DEFAULT_CONVERSATION_SESSION_LIST_LIMIT = 100;
14062
+ var MAX_CONVERSATION_SESSION_LIST_LIMIT = 500;
14063
+ var MAX_CONVERSATION_SESSION_LIST_OFFSET = 1e5;
14064
+ function boundedSessionListInteger(value, name, fallback, minimum, maximum) {
14065
+ if (value === void 0) return fallback;
14066
+ if (!Number.isInteger(value) || value < minimum || value > maximum) {
14067
+ throw new RangeError(
14068
+ `Session list ${name} must be an integer between ${minimum} and ${maximum}.`
14069
+ );
14070
+ }
14071
+ return value;
14072
+ }
13898
14073
  var STANDARD_MODULES_OPERATIONS = [
13899
14074
  {
13900
14075
  create: "entity",
@@ -14233,7 +14408,7 @@ var Environment = class _Environment {
14233
14408
  }
14234
14409
  get sessions() {
14235
14410
  return {
14236
- list: async (options) => this.listSessions(options?.status || "active"),
14411
+ list: async (options = {}) => this.listSessions(options),
14237
14412
  create: async (options) => this.createSession(options),
14238
14413
  connect: async (sessionId, options) => this.connectSession(sessionId, options),
14239
14414
  reopen: async (sessionId, options) => this.reopenSession(sessionId, options),
@@ -14368,17 +14543,12 @@ var Environment = class _Environment {
14368
14543
  */
14369
14544
  async disconnect() {
14370
14545
  }
14371
- async listSessions(status = "active") {
14372
- if (status === "all") {
14373
- const [active, closed] = await Promise.all([
14374
- this.granular.listOpenSessions({ environmentId: this.environmentId }),
14375
- this.granular.listClosedSessions({ environmentId: this.environmentId })
14376
- ]);
14377
- return [...active, ...closed].sort(
14378
- (left, right) => Date.parse(right.lastSeenAt) - Date.parse(left.lastSeenAt)
14379
- );
14380
- }
14381
- return status === "closed" ? this.granular.listClosedSessions({ environmentId: this.environmentId }) : this.granular.listOpenSessions({ environmentId: this.environmentId });
14546
+ async listSessions(optionsOrStatus = {}) {
14547
+ const options = typeof optionsOrStatus === "string" ? { status: optionsOrStatus } : optionsOrStatus;
14548
+ return this.granular.listSessions({
14549
+ ...options,
14550
+ environmentId: this.environmentId
14551
+ });
14382
14552
  }
14383
14553
  async getUserEnvironmentState(options = {}) {
14384
14554
  return this.granular.getUserEnvironmentState({
@@ -14396,6 +14566,7 @@ var Environment = class _Environment {
14396
14566
  return this.granular.createSession({
14397
14567
  environmentId: this.environmentId,
14398
14568
  clientId: options?.clientId,
14569
+ sessionScope: options?.sessionScope,
14399
14570
  initialHeap: options?.initialHeap
14400
14571
  });
14401
14572
  }
@@ -15958,7 +16129,7 @@ var EnvironmentSession = class extends Session {
15958
16129
  * Close only the socket transport without sending `client.goodbye`.
15959
16130
  */
15960
16131
  disconnectTransport() {
15961
- this.client.disconnect();
16132
+ this.client.disconnect({ reason: "Transport detach" });
15962
16133
  }
15963
16134
  /**
15964
16135
  * Backwards-compatible alias for `disconnect()`.
@@ -16353,16 +16524,71 @@ var Granular = class _Granular {
16353
16524
  };
16354
16525
  }
16355
16526
  /**
16356
- * List active (open) sessions for an environment each session is one agent conversation thread.
16527
+ * List indexed sessions using ownership filters and bounded pagination.
16528
+ */
16529
+ async listSessions(options) {
16530
+ const environmentId = options.environmentId?.trim();
16531
+ const sandboxId = options.sandboxId?.trim();
16532
+ const subjectId = options.subjectId?.trim();
16533
+ if (!environmentId && !sandboxId && !subjectId) {
16534
+ throw new Error(
16535
+ "listSessions() requires environmentId, sandboxId, or subjectId so history cannot be scanned accidentally."
16536
+ );
16537
+ }
16538
+ const status = options.status || "active";
16539
+ const allowedStatuses = /* @__PURE__ */ new Set([
16540
+ "active",
16541
+ "closed",
16542
+ "expired",
16543
+ "failed",
16544
+ "timeout",
16545
+ "all"
16546
+ ]);
16547
+ if (!allowedStatuses.has(status)) {
16548
+ throw new Error(`Unsupported session status: ${String(status)}`);
16549
+ }
16550
+ const limit = boundedSessionListInteger(
16551
+ options.limit,
16552
+ "limit",
16553
+ DEFAULT_CONVERSATION_SESSION_LIST_LIMIT,
16554
+ 1,
16555
+ MAX_CONVERSATION_SESSION_LIST_LIMIT
16556
+ );
16557
+ const offset = boundedSessionListInteger(
16558
+ options.offset,
16559
+ "offset",
16560
+ 0,
16561
+ 0,
16562
+ MAX_CONVERSATION_SESSION_LIST_OFFSET
16563
+ );
16564
+ const query = new URLSearchParams({
16565
+ limit: String(limit),
16566
+ offset: String(offset)
16567
+ });
16568
+ if (environmentId) query.set("environmentId", environmentId);
16569
+ if (sandboxId) query.set("sandboxId", sandboxId);
16570
+ if (subjectId) query.set("userId", subjectId);
16571
+ if (options.sessionScope?.trim()) {
16572
+ query.set("sessionScope", options.sessionScope.trim());
16573
+ }
16574
+ if (status !== "all") query.set("status", status);
16575
+ const res = await this.request(
16576
+ `/control/sessions?${query.toString()}`
16577
+ );
16578
+ const items = Array.isArray(res.items) ? res.items : [];
16579
+ return items.map((row) => this.normalizeConversationSession(row));
16580
+ }
16581
+ /**
16582
+ * List active (open) sessions for an environment.
16357
16583
  */
16358
16584
  async listOpenSessions(filters) {
16359
- return this.listSessionsForEnvironment(filters.environmentId, "active");
16585
+ return this.listSessions({ ...filters, status: "active" });
16360
16586
  }
16361
16587
  /**
16362
16588
  * List closed sessions for an environment (conversations that have disconnected).
16363
16589
  */
16364
16590
  async listClosedSessions(filters) {
16365
- return this.listSessionsForEnvironment(filters.environmentId, "closed");
16591
+ return this.listSessions({ ...filters, status: "closed" });
16366
16592
  }
16367
16593
  async getUserEnvironmentState(options) {
16368
16594
  const query = new URLSearchParams({
@@ -16397,14 +16623,6 @@ var Granular = class _Granular {
16397
16623
  });
16398
16624
  return result.readAtBySessionId || {};
16399
16625
  }
16400
- async listSessionsForEnvironment(environmentId, status) {
16401
- const query = new URLSearchParams({ environmentId, status });
16402
- const res = await this.request(
16403
- `/control/sessions?${query.toString()}`
16404
- );
16405
- const items = Array.isArray(res.items) ? res.items : [];
16406
- return items.map((row) => this.normalizeConversationSession(row));
16407
- }
16408
16626
  normalizeConversationSession(row) {
16409
16627
  const sessionId = String(row.sessionId ?? row.session_id ?? "");
16410
16628
  const environmentId = String(row.environmentId ?? row.environment_id ?? "");
@@ -16463,6 +16681,7 @@ var Granular = class _Granular {
16463
16681
  */
16464
16682
  async createSession(options) {
16465
16683
  const clientId = options.clientId || `client_${Date.now()}`;
16684
+ const sessionScope = options.sessionScope?.trim() || void 0;
16466
16685
  await this.activateEnvironment(options.environmentId);
16467
16686
  const envData = await this.environments.get(options.environmentId);
16468
16687
  const environment = this.bindEnvironmentHandle(envData);
@@ -16471,6 +16690,8 @@ var Granular = class _Granular {
16471
16690
  body: JSON.stringify({
16472
16691
  environmentId: options.environmentId,
16473
16692
  clientId,
16693
+ sessionScope,
16694
+ capabilities: sessionScope ? { sessionScope } : void 0,
16474
16695
  initialHeap: options.initialHeap
16475
16696
  })
16476
16697
  });
@@ -19662,6 +19883,7 @@ function buildGranularAgentSystemPrompt(input) {
19662
19883
  - \`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(...)\`.
19663
19884
  - 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.
19664
19885
  - 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()\`.
19886
+ - 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.
19665
19887
  - 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.
19666
19888
  - 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.
19667
19889
  - 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.
@@ -19932,11 +20154,12 @@ ${actionIndex}
19932
20154
  - 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.
19933
20155
  - 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()\`.
19934
20156
  - 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.
19935
- - 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.
20157
+ - 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.
20158
+ - 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.
19936
20159
  - 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.
19937
20160
  - 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.
19938
20161
  - 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.
19939
- - 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.
20162
+ - 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.
19940
20163
  - 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.
19941
20164
  - 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.
19942
20165
  - 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.
@@ -20166,8 +20389,9 @@ function buildContinuationInstructionFromTemplate(resultPreview, options) {
20166
20389
  }
20167
20390
 
20168
20391
  // src/openai-usage.ts
20169
- var OPENAI_PRICING_SOURCE_URL = "https://developers.openai.com/api/docs/models/gpt-5.4/";
20170
- var OPENAI_PRICING_EFFECTIVE_DATE = "2026-05-19";
20392
+ var OPENAI_GPT_5_4_PRICING_SOURCE_URL = "https://developers.openai.com/api/docs/models/gpt-5.4/";
20393
+ var OPENAI_GPT_5_6_PRICING_SOURCE_URL = "https://developers.openai.com/api/docs/pricing";
20394
+ var OPENAI_LONG_CONTEXT_THRESHOLD_TOKENS = 272e3;
20171
20395
  var OPENAI_MODEL_PRICING_USD_PER_MILLION = {
20172
20396
  "gpt-5.4": {
20173
20397
  provider: "openai",
@@ -20176,8 +20400,26 @@ var OPENAI_MODEL_PRICING_USD_PER_MILLION = {
20176
20400
  inputUsdPerMillion: 2.5,
20177
20401
  cachedInputUsdPerMillion: 0.25,
20178
20402
  outputUsdPerMillion: 15,
20179
- sourceUrl: OPENAI_PRICING_SOURCE_URL,
20180
- effectiveDate: OPENAI_PRICING_EFFECTIVE_DATE
20403
+ sourceUrl: OPENAI_GPT_5_4_PRICING_SOURCE_URL,
20404
+ effectiveDate: "2026-05-19"
20405
+ },
20406
+ "gpt-5.6-luna": {
20407
+ provider: "openai",
20408
+ model: "gpt-5.6-luna",
20409
+ currency: "USD",
20410
+ inputUsdPerMillion: 1,
20411
+ cachedInputUsdPerMillion: 0.1,
20412
+ cacheWriteUsdPerMillion: 1.25,
20413
+ outputUsdPerMillion: 6,
20414
+ sourceUrl: OPENAI_GPT_5_6_PRICING_SOURCE_URL,
20415
+ effectiveDate: "2026-07-11",
20416
+ longContextThresholdTokens: OPENAI_LONG_CONTEXT_THRESHOLD_TOKENS,
20417
+ longContextPricing: {
20418
+ inputUsdPerMillion: 2,
20419
+ cachedInputUsdPerMillion: 0.2,
20420
+ cacheWriteUsdPerMillion: 2.5,
20421
+ outputUsdPerMillion: 9
20422
+ }
20181
20423
  }
20182
20424
  };
20183
20425
  function asRecord5(value) {
@@ -20190,8 +20432,18 @@ function numberField(record, key) {
20190
20432
  function microsPerMillion(usdPerMillion) {
20191
20433
  return Math.round(usdPerMillion * 1e6);
20192
20434
  }
20193
- function getOpenAIModelPricing(model) {
20194
- return OPENAI_MODEL_PRICING_USD_PER_MILLION[model] || null;
20435
+ function getOpenAIModelPricing(model, inputTokens = 0) {
20436
+ const pricing = OPENAI_MODEL_PRICING_USD_PER_MILLION[model];
20437
+ if (!pricing) return null;
20438
+ const threshold = pricing.longContextThresholdTokens ?? null;
20439
+ if (pricing.longContextPricing && typeof threshold === "number" && inputTokens > threshold) {
20440
+ return {
20441
+ ...pricing,
20442
+ ...pricing.longContextPricing,
20443
+ contextTier: "long"
20444
+ };
20445
+ }
20446
+ return { ...pricing, contextTier: "short" };
20195
20447
  }
20196
20448
  function normalizeOpenAIUsage(rawUsage) {
20197
20449
  const usage = asRecord5(rawUsage);
@@ -20199,6 +20451,7 @@ function normalizeOpenAIUsage(rawUsage) {
20199
20451
  return {
20200
20452
  inputTokens: 0,
20201
20453
  cachedInputTokens: 0,
20454
+ cacheWriteTokens: 0,
20202
20455
  uncachedInputTokens: 0,
20203
20456
  outputTokens: 0,
20204
20457
  reasoningTokens: 0,
@@ -20214,20 +20467,28 @@ function normalizeOpenAIUsage(rawUsage) {
20214
20467
  inputTokens,
20215
20468
  numberField(inputDetails, "cached_tokens") || numberField(inputDetails, "cached_input_tokens")
20216
20469
  );
20470
+ const cacheWriteTokens = Math.min(
20471
+ Math.max(inputTokens - cachedInputTokens, 0),
20472
+ numberField(inputDetails, "cache_write_tokens")
20473
+ );
20217
20474
  const reasoningTokens = numberField(outputDetails, "reasoning_tokens") || numberField(outputDetails, "reasoning_output_tokens");
20218
20475
  return {
20219
20476
  inputTokens,
20220
20477
  cachedInputTokens,
20221
- uncachedInputTokens: Math.max(inputTokens - cachedInputTokens, 0),
20478
+ cacheWriteTokens,
20479
+ uncachedInputTokens: Math.max(
20480
+ inputTokens - cachedInputTokens - cacheWriteTokens,
20481
+ 0
20482
+ ),
20222
20483
  outputTokens,
20223
20484
  reasoningTokens,
20224
20485
  totalTokens
20225
20486
  };
20226
20487
  }
20227
20488
  function calculateOpenAITokenSpend(model, rawUsage) {
20228
- const pricing = getOpenAIModelPricing(model);
20229
- if (!pricing) return null;
20230
20489
  const usage = normalizeOpenAIUsage(rawUsage);
20490
+ const pricing = getOpenAIModelPricing(model, usage.inputTokens);
20491
+ if (!pricing) return null;
20231
20492
  const inputPricePerMillionMicros = microsPerMillion(
20232
20493
  pricing.inputUsdPerMillion
20233
20494
  );
@@ -20237,14 +20498,19 @@ function calculateOpenAITokenSpend(model, rawUsage) {
20237
20498
  const outputPricePerMillionMicros = microsPerMillion(
20238
20499
  pricing.outputUsdPerMillion
20239
20500
  );
20501
+ const cacheWritePricePerMillionMicros = typeof pricing.cacheWriteUsdPerMillion === "number" ? microsPerMillion(pricing.cacheWriteUsdPerMillion) : null;
20502
+ const cacheWriteCostMicros = Math.round(
20503
+ usage.cacheWriteTokens * (cacheWritePricePerMillionMicros ?? inputPricePerMillionMicros) / 1e6
20504
+ );
20240
20505
  const amountMicros = Math.round(
20241
- (usage.uncachedInputTokens * inputPricePerMillionMicros + usage.cachedInputTokens * cachedInputPricePerMillionMicros + usage.outputTokens * outputPricePerMillionMicros) / 1e6
20506
+ (usage.uncachedInputTokens * inputPricePerMillionMicros + usage.cachedInputTokens * cachedInputPricePerMillionMicros + usage.cacheWriteTokens * (cacheWritePricePerMillionMicros ?? inputPricePerMillionMicros) + usage.outputTokens * outputPricePerMillionMicros) / 1e6
20242
20507
  );
20243
20508
  return {
20244
20509
  provider: "openai",
20245
20510
  model,
20246
20511
  inputTokens: usage.inputTokens,
20247
20512
  cachedInputTokens: usage.cachedInputTokens,
20513
+ cacheWriteTokens: usage.cacheWriteTokens,
20248
20514
  uncachedInputTokens: usage.uncachedInputTokens,
20249
20515
  outputTokens: usage.outputTokens,
20250
20516
  reasoningTokens: usage.reasoningTokens,
@@ -20253,7 +20519,11 @@ function calculateOpenAITokenSpend(model, rawUsage) {
20253
20519
  currency: "USD",
20254
20520
  inputPricePerMillionMicros,
20255
20521
  cachedInputPricePerMillionMicros,
20522
+ cacheWritePricePerMillionMicros,
20523
+ cacheWriteCostMicros,
20256
20524
  outputPricePerMillionMicros,
20525
+ pricingContextTier: pricing.contextTier || "short",
20526
+ longContextThresholdTokens: pricing.longContextThresholdTokens ?? null,
20257
20527
  pricingSource: pricing.sourceUrl,
20258
20528
  pricingEffectiveAt: pricing.effectiveDate,
20259
20529
  usage