@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.js CHANGED
@@ -44,11 +44,11 @@ var __export = (target, all) => {
44
44
  for (var name in all)
45
45
  __defProp(target, name, { get: all[name], enumerable: true });
46
46
  };
47
- var __copyProps = (to, from, except, desc) => {
48
- if (from && typeof from === "object" || typeof from === "function") {
49
- for (let key of __getOwnPropNames(from))
47
+ var __copyProps = (to, from2, except, desc) => {
48
+ if (from2 && typeof from2 === "object" || typeof from2 === "function") {
49
+ for (let key of __getOwnPropNames(from2))
50
50
  if (!__hasOwnProp.call(to, key) && key !== except)
51
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
51
+ __defProp(to, key, { get: () => from2[key], enumerable: !(desc = __getOwnPropDesc(from2, key)) || desc.enumerable });
52
52
  }
53
53
  return to;
54
54
  };
@@ -4065,16 +4065,37 @@ var WSClient = class {
4065
4065
  tokenRefreshTimer = null;
4066
4066
  isExplicitlyDisconnected = false;
4067
4067
  reconnectAttempts = 0;
4068
+ connectPromise = null;
4069
+ connectionEpoch = 0;
4070
+ cancelConnectAttempt = null;
4068
4071
  options;
4069
4072
  constructor(options) {
4070
4073
  this.options = options;
4071
4074
  this.url = options.url;
4072
4075
  this.sessionId = options.sessionId;
4073
4076
  this.token = options.token;
4077
+ if (options.initialDocumentSnapshot) {
4078
+ this.seedDocumentSnapshot(options.initialDocumentSnapshot);
4079
+ }
4074
4080
  }
4075
4081
  get currentSessionId() {
4076
4082
  return this.sessionId;
4077
4083
  }
4084
+ seedDocumentSnapshot(document) {
4085
+ if (!document || typeof document !== "object" || Array.isArray(document)) {
4086
+ return;
4087
+ }
4088
+ try {
4089
+ this.doc = document instanceof Uint8Array ? Automerge__namespace.load(document) : Automerge__namespace.from(document);
4090
+ this.syncState = Automerge__namespace.initSyncState();
4091
+ this.emit("sync", this.doc);
4092
+ } catch (error) {
4093
+ console.warn("[Granular] Failed to seed cached session document", error);
4094
+ }
4095
+ }
4096
+ saveDocumentSnapshot() {
4097
+ return Automerge__namespace.save(this.doc);
4098
+ }
4078
4099
  clearTokenRefreshTimer() {
4079
4100
  if (this.tokenRefreshTimer) {
4080
4101
  clearTimeout(this.tokenRefreshTimer);
@@ -4184,8 +4205,23 @@ var WSClient = class {
4184
4205
  * Connect to the WebSocket server
4185
4206
  * @returns {Promise<void>} Resolves when connection is open
4186
4207
  */
4187
- async connect() {
4208
+ async connect(options = {}) {
4209
+ if (this.ws?.readyState === READY_STATE_OPEN) return;
4210
+ if (this.connectPromise) return this.connectPromise;
4211
+ const connectPromise = this.connectAttempt(options.signal);
4212
+ this.connectPromise = connectPromise;
4213
+ try {
4214
+ await connectPromise;
4215
+ } finally {
4216
+ if (this.connectPromise === connectPromise) {
4217
+ this.connectPromise = null;
4218
+ }
4219
+ }
4220
+ }
4221
+ async connectAttempt(signal) {
4222
+ if (signal?.aborted) throw new Error("WebSocket connect aborted");
4188
4223
  const token = await this.resolveTokenForConnect();
4224
+ if (signal?.aborted) throw new Error("WebSocket connect aborted");
4189
4225
  this.isExplicitlyDisconnected = false;
4190
4226
  this.scheduleTokenRefresh();
4191
4227
  if (this.reconnectTimer) {
@@ -4197,7 +4233,7 @@ var WSClient = class {
4197
4233
  try {
4198
4234
  const wsModule = await Promise.resolve().then(() => (init_wrapper(), wrapper_exports));
4199
4235
  WebSocketClass = wsModule.default || wsModule;
4200
- } catch (e) {
4236
+ } catch {
4201
4237
  }
4202
4238
  }
4203
4239
  if (!WebSocketClass) {
@@ -4205,83 +4241,97 @@ var WSClient = class {
4205
4241
  'No WebSocket implementation found. If using Node.js, please install "ws" and pass the constructor to the SDK options: { WebSocketCtor: WebSocket }.'
4206
4242
  );
4207
4243
  }
4244
+ const epoch = ++this.connectionEpoch;
4245
+ const wsUrl = new URL(this.url);
4246
+ wsUrl.searchParams.set("sessionId", this.sessionId);
4247
+ wsUrl.searchParams.set("token", token);
4248
+ const socket = new WebSocketClass(wsUrl.toString());
4249
+ this.ws = socket;
4208
4250
  return new Promise((resolve, reject) => {
4209
- try {
4210
- const wsUrl = new URL(this.url);
4211
- wsUrl.searchParams.set("sessionId", this.sessionId);
4212
- wsUrl.searchParams.set("token", token);
4213
- this.ws = new WebSocketClass(wsUrl.toString());
4214
- if (!this.ws) throw new Error("Failed to create WebSocket");
4215
- const socket = this.ws;
4216
- if (typeof socket.on === "function") {
4217
- socket.on("open", () => {
4218
- if (this.reconnectTimer) {
4219
- clearTimeout(this.reconnectTimer);
4220
- this.reconnectTimer = null;
4221
- }
4222
- this.reconnectAttempts = 0;
4223
- this.emit("open", {});
4224
- resolve();
4225
- });
4226
- socket.on("message", (data) => {
4227
- try {
4228
- const message = JSON.parse(data.toString());
4229
- this.handleMessage(message);
4230
- } catch (error) {
4231
- console.error("[Granular] Failed to parse message:", error);
4232
- }
4233
- });
4234
- socket.on("error", (error) => {
4235
- this.emit("error", error);
4236
- if (socket.readyState !== READY_STATE_OPEN) {
4237
- reject(error);
4238
- }
4239
- });
4240
- socket.on("close", (code, reason) => {
4241
- this.handleDisconnect({
4242
- code,
4243
- reason: this.normalizeReason(reason),
4244
- // ws does not provide wasClean on Node-style close callback
4245
- wasClean: code === 1e3
4246
- });
4247
- });
4251
+ let settled = false;
4252
+ const isCurrent = () => this.connectionEpoch === epoch && this.ws === socket;
4253
+ const finish = (error) => {
4254
+ if (settled) return;
4255
+ settled = true;
4256
+ if (this.cancelConnectAttempt === handleAbort) {
4257
+ this.cancelConnectAttempt = null;
4258
+ }
4259
+ signal?.removeEventListener("abort", handleAbort);
4260
+ if (error) {
4261
+ reject(error instanceof Error ? error : new Error(String(error)));
4248
4262
  } else {
4249
- this.ws.onopen = () => {
4250
- if (this.reconnectTimer) {
4251
- clearTimeout(this.reconnectTimer);
4252
- this.reconnectTimer = null;
4253
- }
4254
- this.reconnectAttempts = 0;
4255
- this.emit("open", {});
4256
- resolve();
4257
- };
4258
- this.ws.onmessage = (event) => {
4259
- try {
4260
- const data = event.data;
4261
- const message = JSON.parse(data.toString());
4262
- this.handleMessage(message);
4263
- } catch (error) {
4264
- console.error("[Granular] Failed to parse message:", error);
4265
- }
4266
- };
4267
- this.ws.onerror = (event) => {
4268
- const error = new Error("WebSocket error");
4269
- error.event = event;
4270
- this.emit("error", error);
4271
- if (this.ws?.readyState !== READY_STATE_OPEN) {
4272
- reject(error);
4273
- }
4274
- };
4275
- this.ws.onclose = (event) => {
4276
- this.handleDisconnect({
4277
- code: event.code,
4278
- reason: event.reason,
4279
- wasClean: event.wasClean
4280
- });
4281
- };
4263
+ resolve();
4282
4264
  }
4283
- } catch (error) {
4284
- reject(error);
4265
+ };
4266
+ const closeStaleSocket = () => {
4267
+ try {
4268
+ socket.close(1e3, "Stale connection attempt");
4269
+ } catch {
4270
+ }
4271
+ };
4272
+ const handleAbort = () => {
4273
+ if (isCurrent()) {
4274
+ this.connectionEpoch += 1;
4275
+ this.ws = null;
4276
+ }
4277
+ closeStaleSocket();
4278
+ finish(new Error("WebSocket connect aborted"));
4279
+ };
4280
+ this.cancelConnectAttempt = handleAbort;
4281
+ const handleOpen = () => {
4282
+ if (!isCurrent()) {
4283
+ closeStaleSocket();
4284
+ return;
4285
+ }
4286
+ this.reconnectAttempts = 0;
4287
+ this.emit("open", {});
4288
+ finish();
4289
+ };
4290
+ const handleMessage = (data) => {
4291
+ if (!isCurrent()) return;
4292
+ try {
4293
+ const text = typeof data === "string" ? data : data && typeof data === "object" && "toString" in data ? String(data.toString()) : "";
4294
+ this.handleMessage(JSON.parse(text));
4295
+ } catch (error) {
4296
+ console.error("[Granular] Failed to parse message:", error);
4297
+ }
4298
+ };
4299
+ const handleError = (error) => {
4300
+ if (!isCurrent()) return;
4301
+ const typedError = error instanceof Error ? error : new Error("WebSocket error");
4302
+ this.emit("error", typedError);
4303
+ if (socket.readyState !== READY_STATE_OPEN) finish(typedError);
4304
+ };
4305
+ const handleClose = (close) => {
4306
+ if (!isCurrent()) return;
4307
+ if (!settled) {
4308
+ finish(
4309
+ new Error(
4310
+ `WebSocket closed before ready${close.code ? ` (code=${close.code})` : ""}`
4311
+ )
4312
+ );
4313
+ }
4314
+ this.handleDisconnect({
4315
+ code: close.code,
4316
+ reason: this.normalizeReason(close.reason),
4317
+ wasClean: close.wasClean
4318
+ });
4319
+ };
4320
+ signal?.addEventListener("abort", handleAbort, { once: true });
4321
+ const nodeSocket = socket;
4322
+ if (typeof nodeSocket.on === "function") {
4323
+ nodeSocket.on("open", handleOpen);
4324
+ nodeSocket.on("message", handleMessage);
4325
+ nodeSocket.on("error", handleError);
4326
+ nodeSocket.on(
4327
+ "close",
4328
+ (code, reason) => handleClose({ code, reason, wasClean: code === 1e3 })
4329
+ );
4330
+ } else {
4331
+ socket.onopen = handleOpen;
4332
+ socket.onmessage = (event) => handleMessage(event.data);
4333
+ socket.onerror = handleError;
4334
+ socket.onclose = (event) => handleClose(event);
4285
4335
  }
4286
4336
  });
4287
4337
  }
@@ -4299,9 +4349,58 @@ var WSClient = class {
4299
4349
  return void 0;
4300
4350
  }
4301
4351
  rejectPending(error) {
4302
- this.messageQueue.forEach((pending) => pending.reject(error));
4352
+ this.messageQueue.forEach((pending) => {
4353
+ clearTimeout(pending.timeout);
4354
+ pending.reject(error);
4355
+ });
4303
4356
  this.messageQueue = [];
4304
4357
  }
4358
+ emitReconnectErrorMessage(error) {
4359
+ const reconnectInfo = {
4360
+ error,
4361
+ sessionId: this.sessionId,
4362
+ timestamp: Date.now()
4363
+ };
4364
+ this.emit("reconnect_error", reconnectInfo);
4365
+ if (this.options.onReconnectError) {
4366
+ try {
4367
+ this.options.onReconnectError(reconnectInfo);
4368
+ } catch (callbackError) {
4369
+ console.error(
4370
+ "[Granular] onReconnectError callback failed:",
4371
+ callbackError
4372
+ );
4373
+ }
4374
+ }
4375
+ }
4376
+ scheduleReconnectAttempt() {
4377
+ if (this.isExplicitlyDisconnected || this.reconnectTimer) return null;
4378
+ const baseReconnectDelayMs = typeof this.options.reconnectDelayMs === "number" && Number.isFinite(this.options.reconnectDelayMs) && this.options.reconnectDelayMs > 0 ? this.options.reconnectDelayMs : DEFAULT_RECONNECT_DELAY_MS;
4379
+ 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;
4380
+ if (this.reconnectAttempts >= maxReconnectAttempts) {
4381
+ this.emitReconnectErrorMessage(
4382
+ `WebSocket reconnect attempts exhausted after ${maxReconnectAttempts} attempt(s).`
4383
+ );
4384
+ return null;
4385
+ }
4386
+ this.reconnectAttempts += 1;
4387
+ const reconnectDelayMs = Math.min(
4388
+ 3e4,
4389
+ baseReconnectDelayMs * 2 ** Math.max(0, this.reconnectAttempts - 1)
4390
+ );
4391
+ this.reconnectTimer = setTimeout(() => {
4392
+ this.reconnectTimer = null;
4393
+ console.log("[Granular] Attempting reconnect...");
4394
+ this.connect().catch((error) => {
4395
+ console.error("[Granular] Reconnect failed:", error);
4396
+ this.emitReconnectErrorMessage(
4397
+ error instanceof Error ? error.message : String(error)
4398
+ );
4399
+ this.scheduleReconnectAttempt();
4400
+ });
4401
+ }, reconnectDelayMs);
4402
+ return reconnectDelayMs;
4403
+ }
4305
4404
  buildDisconnectError(info) {
4306
4405
  const details = [
4307
4406
  info.code !== void 0 ? `code=${info.code}` : void 0,
@@ -4311,8 +4410,6 @@ var WSClient = class {
4311
4410
  return new Error(`WebSocket disconnected${suffix}`);
4312
4411
  }
4313
4412
  handleDisconnect(close = {}) {
4314
- const baseReconnectDelayMs = typeof this.options.reconnectDelayMs === "number" && Number.isFinite(this.options.reconnectDelayMs) && this.options.reconnectDelayMs > 0 ? this.options.reconnectDelayMs : DEFAULT_RECONNECT_DELAY_MS;
4315
- 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;
4316
4413
  const unexpected = !this.isExplicitlyDisconnected;
4317
4414
  const info = {
4318
4415
  code: close.code,
@@ -4332,32 +4429,9 @@ var WSClient = class {
4332
4429
  const disconnectError = this.buildDisconnectError(info);
4333
4430
  this.rejectPending(disconnectError);
4334
4431
  this.emit("disconnect", info);
4335
- if (this.reconnectAttempts >= maxReconnectAttempts) {
4336
- const reconnectInfo = {
4337
- error: `WebSocket reconnect attempts exhausted after ${maxReconnectAttempts} attempt(s).`,
4338
- sessionId: this.sessionId,
4339
- timestamp: Date.now()
4340
- };
4341
- this.emit("reconnect_error", reconnectInfo);
4342
- if (this.options.onReconnectError) {
4343
- try {
4344
- this.options.onReconnectError(reconnectInfo);
4345
- } catch (callbackError) {
4346
- console.error(
4347
- "[Granular] onReconnectError callback failed:",
4348
- callbackError
4349
- );
4350
- }
4351
- }
4352
- return;
4353
- }
4354
- this.reconnectAttempts += 1;
4355
- const reconnectDelayMs = Math.min(
4356
- 3e4,
4357
- baseReconnectDelayMs * 2 ** Math.max(0, this.reconnectAttempts - 1)
4358
- );
4359
- info.reconnectScheduled = true;
4360
- info.reconnectDelayMs = reconnectDelayMs;
4432
+ const reconnectDelayMs = this.scheduleReconnectAttempt();
4433
+ info.reconnectScheduled = reconnectDelayMs !== null;
4434
+ if (reconnectDelayMs !== null) info.reconnectDelayMs = reconnectDelayMs;
4361
4435
  if (this.options.onUnexpectedClose) {
4362
4436
  try {
4363
4437
  this.options.onUnexpectedClose(info);
@@ -4368,28 +4442,6 @@ var WSClient = class {
4368
4442
  );
4369
4443
  }
4370
4444
  }
4371
- this.reconnectTimer = setTimeout(() => {
4372
- console.log("[Granular] Attempting reconnect...");
4373
- this.connect().catch((error) => {
4374
- console.error("[Granular] Reconnect failed:", error);
4375
- const reconnectInfo = {
4376
- error: error instanceof Error ? error.message : String(error),
4377
- sessionId: this.sessionId,
4378
- timestamp: Date.now()
4379
- };
4380
- this.emit("reconnect_error", reconnectInfo);
4381
- if (this.options.onReconnectError) {
4382
- try {
4383
- this.options.onReconnectError(reconnectInfo);
4384
- } catch (callbackError) {
4385
- console.error(
4386
- "[Granular] onReconnectError callback failed:",
4387
- callbackError
4388
- );
4389
- }
4390
- }
4391
- });
4392
- }, reconnectDelayMs);
4393
4445
  }
4394
4446
  }
4395
4447
  handleMessage(message) {
@@ -4500,6 +4552,7 @@ var WSClient = class {
4500
4552
  const response = message;
4501
4553
  const pending = this.messageQueue.find((q) => q.id === response.id);
4502
4554
  if (pending) {
4555
+ clearTimeout(pending.timeout);
4503
4556
  if (response.type === "rpc_error") {
4504
4557
  pending.reject(
4505
4558
  new Error(
@@ -4545,16 +4598,22 @@ var WSClient = class {
4545
4598
  id
4546
4599
  };
4547
4600
  return new Promise((resolve, reject) => {
4548
- this.messageQueue.push({ resolve, reject, id });
4549
- this.ws.send(JSON.stringify(request));
4550
4601
  const timeoutMs = rpcTimeoutMsForMethod(method);
4551
- setTimeout(() => {
4602
+ const timeout = setTimeout(() => {
4552
4603
  const pending = this.messageQueue.find((q) => q.id === id);
4553
4604
  if (pending) {
4554
4605
  this.messageQueue = this.messageQueue.filter((q) => q.id !== id);
4555
4606
  reject(new Error(`RPC timeout: ${method}`));
4556
4607
  }
4557
4608
  }, timeoutMs);
4609
+ this.messageQueue.push({ resolve, reject, id, timeout });
4610
+ try {
4611
+ this.ws.send(JSON.stringify(request));
4612
+ } catch (error) {
4613
+ clearTimeout(timeout);
4614
+ this.messageQueue = this.messageQueue.filter((q) => q.id !== id);
4615
+ reject(error instanceof Error ? error : new Error(String(error)));
4616
+ }
4558
4617
  });
4559
4618
  }
4560
4619
  async handleIncomingRpc(request) {
@@ -4640,15 +4699,18 @@ var WSClient = class {
4640
4699
  /**
4641
4700
  * Disconnect the WebSocket and clear state
4642
4701
  */
4643
- disconnect() {
4702
+ disconnect(options = {}) {
4644
4703
  this.isExplicitlyDisconnected = true;
4704
+ this.cancelConnectAttempt?.();
4705
+ this.cancelConnectAttempt = null;
4706
+ this.connectionEpoch += 1;
4645
4707
  if (this.reconnectTimer) {
4646
4708
  clearTimeout(this.reconnectTimer);
4647
4709
  this.reconnectTimer = null;
4648
4710
  }
4649
4711
  this.clearTokenRefreshTimer();
4650
4712
  if (this.ws) {
4651
- this.ws.close(1e3, "Client disconnect");
4713
+ this.ws.close(1e3, options.reason || "Client disconnect");
4652
4714
  this.ws = null;
4653
4715
  }
4654
4716
  this.rejectPending(new Error("Client explicitly disconnected"));
@@ -4744,8 +4806,12 @@ function normalizePrompt(rawValue) {
4744
4806
  const source = promptRecord || raw;
4745
4807
  const id = typeof source.id === "string" ? source.id : typeof raw.id === "string" ? raw.id : typeof raw.promptId === "string" ? raw.promptId : "";
4746
4808
  if (!id) return null;
4809
+ const jobId = typeof source.jobId === "string" && source.jobId.trim() ? source.jobId.trim() : typeof raw.jobId === "string" && raw.jobId.trim() ? raw.jobId.trim() : void 0;
4810
+ const turnId = typeof source.turnId === "string" && source.turnId.trim() ? source.turnId.trim() : typeof raw.turnId === "string" && raw.turnId.trim() ? raw.turnId.trim() : void 0;
4747
4811
  return {
4748
4812
  id,
4813
+ ...jobId ? { jobId } : {},
4814
+ ...turnId ? { turnId } : {},
4749
4815
  type: normalizePromptType(source === raw ? raw : { ...raw, ...source }),
4750
4816
  title: typeof source.title === "string" ? source.title : "Input required",
4751
4817
  message: typeof source.message === "string" ? source.message : "",
@@ -4832,6 +4898,9 @@ var Session = class {
4832
4898
  this.initialQuota = options.initialQuota || null;
4833
4899
  this.setupEventHandlers();
4834
4900
  this.setupToolInvokeHandler();
4901
+ this.currentDomainRevision = this.extractDomainRevisionFromDoc(
4902
+ this.client.doc
4903
+ );
4835
4904
  }
4836
4905
  extractDomainRevisionFromDoc(doc) {
4837
4906
  const domain = doc?.domain;
@@ -5737,6 +5806,7 @@ function normalizeJobAgentMessageEnvelope(data) {
5737
5806
  }
5738
5807
  return {
5739
5808
  jobId: d.jobId,
5809
+ ...typeof d.turnId === "string" && d.turnId.trim() ? { turnId: d.turnId.trim() } : {},
5740
5810
  message: {
5741
5811
  messageId: d.messageId,
5742
5812
  kind: d.kind === "artifacts" ? "artifacts" : "text",
@@ -6395,9 +6465,14 @@ function normalizeShowRefs(value) {
6395
6465
  variableNames: normalizeRefs(record.variableNames),
6396
6466
  fileIds: normalizeRefs(record.fileIds),
6397
6467
  sessionArtifactIds: normalizeRefs(record.sessionArtifactIds),
6398
- actionSuggestions: normalizeActionSuggestions(record.actionSuggestions)
6468
+ actionSuggestions: normalizeActionSuggestions(record.actionSuggestions),
6469
+ tables: Array.isArray(record.tables) ? record.tables.filter(
6470
+ (table) => Boolean(
6471
+ table && typeof table === "object" && !Array.isArray(table) && Array.isArray(table.columns) && Array.isArray(table.rows)
6472
+ )
6473
+ ) : void 0
6399
6474
  };
6400
- return show.entryPaths || show.listNames || show.variableNames || show.fileIds || show.sessionArtifactIds || show.actionSuggestions ? show : void 0;
6475
+ return show.entryPaths || show.listNames || show.variableNames || show.fileIds || show.sessionArtifactIds || show.actionSuggestions || show.tables ? show : void 0;
6401
6476
  }
6402
6477
  function normalizeActionSuggestions(value) {
6403
6478
  if (!Array.isArray(value)) return void 0;
@@ -6419,6 +6494,76 @@ function normalizeActionSuggestions(value) {
6419
6494
  }
6420
6495
  return suggestions.length ? suggestions : void 0;
6421
6496
  }
6497
+ var TRANSCRIPT_MESSAGE_PART_LIMIT = 128;
6498
+ var TRANSCRIPT_MESSAGE_PART_TEXT_LIMIT = 2e5;
6499
+ var TRANSCRIPT_MESSAGE_PART_ACTION_LABEL_LIMIT = 1e3;
6500
+ var TRANSCRIPT_MESSAGE_ACTION_LIMIT = 64;
6501
+ function normalizeConversationMessageActions(value) {
6502
+ if (!Array.isArray(value) || value.length === 0) return void 0;
6503
+ const actions = [];
6504
+ for (const item of value.slice(0, TRANSCRIPT_MESSAGE_ACTION_LIMIT)) {
6505
+ const record = asRecord3(item);
6506
+ const kind = record?.kind;
6507
+ const label = trimString(record?.label ?? record?.title);
6508
+ const status = record?.status;
6509
+ if (kind !== "frontend" && kind !== "backend" && kind !== "system" || !label || label.length > TRANSCRIPT_MESSAGE_PART_ACTION_LABEL_LIMIT) {
6510
+ continue;
6511
+ }
6512
+ actions.push({
6513
+ kind,
6514
+ label,
6515
+ ...status === "done" || status === "queued" || status === "failed" ? { status } : {}
6516
+ });
6517
+ }
6518
+ return actions.length ? actions : void 0;
6519
+ }
6520
+ function normalizeConversationMessageParts(value, canonicalContent, canonicalActions) {
6521
+ if (!Array.isArray(value) || value.length === 0 || value.length > TRANSCRIPT_MESSAGE_PART_LIMIT) {
6522
+ return void 0;
6523
+ }
6524
+ const parts = [];
6525
+ const canonicalActionsById = new Map(
6526
+ (canonicalActions || []).map((action) => [
6527
+ `${action.kind}:${action.label}`,
6528
+ action
6529
+ ])
6530
+ );
6531
+ const seenActionIds = /* @__PURE__ */ new Set();
6532
+ let textLength = 0;
6533
+ for (const item of value) {
6534
+ const record = asRecord3(item);
6535
+ if (!record) return void 0;
6536
+ if (record.type === "text") {
6537
+ if (typeof record.text !== "string" || record.text.length === 0) {
6538
+ return void 0;
6539
+ }
6540
+ textLength += record.text.length;
6541
+ if (textLength > TRANSCRIPT_MESSAGE_PART_TEXT_LIMIT) return void 0;
6542
+ parts.push({ type: "text", text: record.text });
6543
+ continue;
6544
+ }
6545
+ if (record.type !== "action") return void 0;
6546
+ const action = asRecord3(record.action);
6547
+ const kind = action?.kind;
6548
+ const label = trimString(action?.label);
6549
+ if (kind !== "frontend" && kind !== "backend" && kind !== "system" || !label || label.length > TRANSCRIPT_MESSAGE_PART_ACTION_LABEL_LIMIT) {
6550
+ return void 0;
6551
+ }
6552
+ const actionId = `${kind}:${label}`;
6553
+ const canonicalAction = canonicalActionsById.get(actionId);
6554
+ if (!canonicalAction) return void 0;
6555
+ if (seenActionIds.has(actionId)) continue;
6556
+ seenActionIds.add(actionId);
6557
+ parts.push({
6558
+ type: "action",
6559
+ action: canonicalAction
6560
+ });
6561
+ }
6562
+ const orderedText = parts.filter(
6563
+ (part) => part.type === "text"
6564
+ ).map((part) => part.text).join("");
6565
+ return orderedText === canonicalContent ? parts : void 0;
6566
+ }
6422
6567
  function stringifyTranscriptValue(value, fallback = "") {
6423
6568
  if (typeof value === "string") {
6424
6569
  return value.trim() || fallback;
@@ -6577,10 +6722,12 @@ function normalizeConversationMessage(raw, artifactsById) {
6577
6722
  const content = trimString(
6578
6723
  record.content ?? record.reply ?? record.message ?? record.text
6579
6724
  );
6725
+ const actions = role === "assistant" ? normalizeConversationMessageActions(record.actions) : void 0;
6726
+ const parts = role === "assistant" ? normalizeConversationMessageParts(record.parts, content, actions) : void 0;
6580
6727
  const show = normalizeShowRefs(record.show);
6581
6728
  const id = asString(record.id) || crypto.randomUUID();
6582
6729
  const timestamp = asNumber(record.timestamp) || asNumber(record.ts) || 0;
6583
- if (!content && !show) return null;
6730
+ if (!content && !show && !actions?.length) return null;
6584
6731
  const artifactHistory = buildArtifactHistory(show, artifactsById);
6585
6732
  const historyContent = role === "assistant" ? content && artifactHistory ? `[Assistant reply]
6586
6733
  ${content}
@@ -6595,6 +6742,8 @@ ${content}` : artifactHistory : void 0;
6595
6742
  jobId: asString(record.jobId),
6596
6743
  promptId: asString(record.promptId),
6597
6744
  show,
6745
+ actions,
6746
+ parts,
6598
6747
  historyContent,
6599
6748
  source: "conversation"
6600
6749
  };
@@ -12172,7 +12321,12 @@ async function recordOpenAIUsageSpend(options) {
12172
12321
  const metadata = {
12173
12322
  ...options.metadata || {},
12174
12323
  ...options.usage.rawUsage !== void 0 ? { openaiUsage: options.usage.rawUsage } : {},
12175
- usageContext: context
12324
+ usageContext: context,
12325
+ pricingContextTier: options.usage.pricingContextTier,
12326
+ cacheWritePricePerMillionMicros: options.usage.cacheWritePricePerMillionMicros,
12327
+ cacheWriteTokens: options.usage.cacheWriteTokens,
12328
+ cacheWriteCostMicros: options.usage.cacheWriteCostMicros,
12329
+ longContextThresholdTokens: options.usage.longContextThresholdTokens
12176
12330
  };
12177
12331
  const response = await fetch(
12178
12332
  `${toGranularHttpBase(options.apiUrl)}/control/spend/events`,
@@ -13100,11 +13254,20 @@ function buildStateMachineModelMutations(modelPath, machines) {
13100
13254
  return mutations;
13101
13255
  }
13102
13256
  function buildMachineTypes(classSummary, machine) {
13257
+ const stateGlossary = machine.states.map((state) => {
13258
+ const label = state.label && state.label !== state.name ? state.label : null;
13259
+ const meaning = [label, state.description].filter(Boolean).join(" \u2014 ");
13260
+ const finalMarker = state.isFinal ? " Final state." : "";
13261
+ return `${state.name}${meaning ? `: ${meaning}` : "."}${finalMarker}`;
13262
+ });
13103
13263
  return [
13104
13264
  {
13105
13265
  kind: "union",
13106
13266
  name: stateTypeName(classSummary.name, machine.name),
13107
- docs: [`Allowed states for ${classSummary.name}.${machine.name}.`],
13267
+ docs: [
13268
+ `Allowed states for ${classSummary.name}.${machine.name}.`,
13269
+ ...stateGlossary
13270
+ ],
13108
13271
  members: machine.states.map((state) => state.name)
13109
13272
  },
13110
13273
  {
@@ -13452,7 +13615,7 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
13452
13615
  },
13453
13616
  add_transition: async (value, {
13454
13617
  name,
13455
- from,
13618
+ from: from2,
13456
13619
  to,
13457
13620
  label,
13458
13621
  description,
@@ -13467,7 +13630,7 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
13467
13630
  value.target.add_state_machine_transition(
13468
13631
  value.name,
13469
13632
  name,
13470
- from,
13633
+ from2,
13471
13634
  to,
13472
13635
  {
13473
13636
  label,
@@ -13917,6 +14080,18 @@ function buildEffectMetamodelMutations(toolPath, spec) {
13917
14080
  }
13918
14081
 
13919
14082
  // src/client.ts
14083
+ var DEFAULT_CONVERSATION_SESSION_LIST_LIMIT = 100;
14084
+ var MAX_CONVERSATION_SESSION_LIST_LIMIT = 500;
14085
+ var MAX_CONVERSATION_SESSION_LIST_OFFSET = 1e5;
14086
+ function boundedSessionListInteger(value, name, fallback, minimum, maximum) {
14087
+ if (value === void 0) return fallback;
14088
+ if (!Number.isInteger(value) || value < minimum || value > maximum) {
14089
+ throw new RangeError(
14090
+ `Session list ${name} must be an integer between ${minimum} and ${maximum}.`
14091
+ );
14092
+ }
14093
+ return value;
14094
+ }
13920
14095
  var STANDARD_MODULES_OPERATIONS = [
13921
14096
  {
13922
14097
  create: "entity",
@@ -14255,7 +14430,7 @@ var Environment = class _Environment {
14255
14430
  }
14256
14431
  get sessions() {
14257
14432
  return {
14258
- list: async (options) => this.listSessions(options?.status || "active"),
14433
+ list: async (options = {}) => this.listSessions(options),
14259
14434
  create: async (options) => this.createSession(options),
14260
14435
  connect: async (sessionId, options) => this.connectSession(sessionId, options),
14261
14436
  reopen: async (sessionId, options) => this.reopenSession(sessionId, options),
@@ -14390,17 +14565,12 @@ var Environment = class _Environment {
14390
14565
  */
14391
14566
  async disconnect() {
14392
14567
  }
14393
- async listSessions(status = "active") {
14394
- if (status === "all") {
14395
- const [active, closed] = await Promise.all([
14396
- this.granular.listOpenSessions({ environmentId: this.environmentId }),
14397
- this.granular.listClosedSessions({ environmentId: this.environmentId })
14398
- ]);
14399
- return [...active, ...closed].sort(
14400
- (left, right) => Date.parse(right.lastSeenAt) - Date.parse(left.lastSeenAt)
14401
- );
14402
- }
14403
- return status === "closed" ? this.granular.listClosedSessions({ environmentId: this.environmentId }) : this.granular.listOpenSessions({ environmentId: this.environmentId });
14568
+ async listSessions(optionsOrStatus = {}) {
14569
+ const options = typeof optionsOrStatus === "string" ? { status: optionsOrStatus } : optionsOrStatus;
14570
+ return this.granular.listSessions({
14571
+ ...options,
14572
+ environmentId: this.environmentId
14573
+ });
14404
14574
  }
14405
14575
  async getUserEnvironmentState(options = {}) {
14406
14576
  return this.granular.getUserEnvironmentState({
@@ -14418,6 +14588,7 @@ var Environment = class _Environment {
14418
14588
  return this.granular.createSession({
14419
14589
  environmentId: this.environmentId,
14420
14590
  clientId: options?.clientId,
14591
+ sessionScope: options?.sessionScope,
14421
14592
  initialHeap: options?.initialHeap
14422
14593
  });
14423
14594
  }
@@ -15980,7 +16151,7 @@ var EnvironmentSession = class extends Session {
15980
16151
  * Close only the socket transport without sending `client.goodbye`.
15981
16152
  */
15982
16153
  disconnectTransport() {
15983
- this.client.disconnect();
16154
+ this.client.disconnect({ reason: "Transport detach" });
15984
16155
  }
15985
16156
  /**
15986
16157
  * Backwards-compatible alias for `disconnect()`.
@@ -16375,16 +16546,71 @@ var Granular = class _Granular {
16375
16546
  };
16376
16547
  }
16377
16548
  /**
16378
- * List active (open) sessions for an environment each session is one agent conversation thread.
16549
+ * List indexed sessions using ownership filters and bounded pagination.
16550
+ */
16551
+ async listSessions(options) {
16552
+ const environmentId = options.environmentId?.trim();
16553
+ const sandboxId = options.sandboxId?.trim();
16554
+ const subjectId = options.subjectId?.trim();
16555
+ if (!environmentId && !sandboxId && !subjectId) {
16556
+ throw new Error(
16557
+ "listSessions() requires environmentId, sandboxId, or subjectId so history cannot be scanned accidentally."
16558
+ );
16559
+ }
16560
+ const status = options.status || "active";
16561
+ const allowedStatuses = /* @__PURE__ */ new Set([
16562
+ "active",
16563
+ "closed",
16564
+ "expired",
16565
+ "failed",
16566
+ "timeout",
16567
+ "all"
16568
+ ]);
16569
+ if (!allowedStatuses.has(status)) {
16570
+ throw new Error(`Unsupported session status: ${String(status)}`);
16571
+ }
16572
+ const limit = boundedSessionListInteger(
16573
+ options.limit,
16574
+ "limit",
16575
+ DEFAULT_CONVERSATION_SESSION_LIST_LIMIT,
16576
+ 1,
16577
+ MAX_CONVERSATION_SESSION_LIST_LIMIT
16578
+ );
16579
+ const offset = boundedSessionListInteger(
16580
+ options.offset,
16581
+ "offset",
16582
+ 0,
16583
+ 0,
16584
+ MAX_CONVERSATION_SESSION_LIST_OFFSET
16585
+ );
16586
+ const query = new URLSearchParams({
16587
+ limit: String(limit),
16588
+ offset: String(offset)
16589
+ });
16590
+ if (environmentId) query.set("environmentId", environmentId);
16591
+ if (sandboxId) query.set("sandboxId", sandboxId);
16592
+ if (subjectId) query.set("userId", subjectId);
16593
+ if (options.sessionScope?.trim()) {
16594
+ query.set("sessionScope", options.sessionScope.trim());
16595
+ }
16596
+ if (status !== "all") query.set("status", status);
16597
+ const res = await this.request(
16598
+ `/control/sessions?${query.toString()}`
16599
+ );
16600
+ const items = Array.isArray(res.items) ? res.items : [];
16601
+ return items.map((row) => this.normalizeConversationSession(row));
16602
+ }
16603
+ /**
16604
+ * List active (open) sessions for an environment.
16379
16605
  */
16380
16606
  async listOpenSessions(filters) {
16381
- return this.listSessionsForEnvironment(filters.environmentId, "active");
16607
+ return this.listSessions({ ...filters, status: "active" });
16382
16608
  }
16383
16609
  /**
16384
16610
  * List closed sessions for an environment (conversations that have disconnected).
16385
16611
  */
16386
16612
  async listClosedSessions(filters) {
16387
- return this.listSessionsForEnvironment(filters.environmentId, "closed");
16613
+ return this.listSessions({ ...filters, status: "closed" });
16388
16614
  }
16389
16615
  async getUserEnvironmentState(options) {
16390
16616
  const query = new URLSearchParams({
@@ -16419,14 +16645,6 @@ var Granular = class _Granular {
16419
16645
  });
16420
16646
  return result.readAtBySessionId || {};
16421
16647
  }
16422
- async listSessionsForEnvironment(environmentId, status) {
16423
- const query = new URLSearchParams({ environmentId, status });
16424
- const res = await this.request(
16425
- `/control/sessions?${query.toString()}`
16426
- );
16427
- const items = Array.isArray(res.items) ? res.items : [];
16428
- return items.map((row) => this.normalizeConversationSession(row));
16429
- }
16430
16648
  normalizeConversationSession(row) {
16431
16649
  const sessionId = String(row.sessionId ?? row.session_id ?? "");
16432
16650
  const environmentId = String(row.environmentId ?? row.environment_id ?? "");
@@ -16485,6 +16703,7 @@ var Granular = class _Granular {
16485
16703
  */
16486
16704
  async createSession(options) {
16487
16705
  const clientId = options.clientId || `client_${Date.now()}`;
16706
+ const sessionScope = options.sessionScope?.trim() || void 0;
16488
16707
  await this.activateEnvironment(options.environmentId);
16489
16708
  const envData = await this.environments.get(options.environmentId);
16490
16709
  const environment = this.bindEnvironmentHandle(envData);
@@ -16493,6 +16712,8 @@ var Granular = class _Granular {
16493
16712
  body: JSON.stringify({
16494
16713
  environmentId: options.environmentId,
16495
16714
  clientId,
16715
+ sessionScope,
16716
+ capabilities: sessionScope ? { sessionScope } : void 0,
16496
16717
  initialHeap: options.initialHeap
16497
16718
  })
16498
16719
  });
@@ -19684,6 +19905,7 @@ function buildGranularAgentSystemPrompt(input) {
19684
19905
  - \`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(...)\`.
19685
19906
  - 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.
19686
19907
  - 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()\`.
19908
+ - 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.
19687
19909
  - 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.
19688
19910
  - 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.
19689
19911
  - 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.
@@ -19954,11 +20176,12 @@ ${actionIndex}
19954
20176
  - 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.
19955
20177
  - 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()\`.
19956
20178
  - 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.
19957
- - 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.
20179
+ - 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.
20180
+ - 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.
19958
20181
  - 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.
19959
20182
  - 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.
19960
20183
  - 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.
19961
- - 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.
20184
+ - 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.
19962
20185
  - 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.
19963
20186
  - 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.
19964
20187
  - 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.
@@ -20188,8 +20411,9 @@ function buildContinuationInstructionFromTemplate(resultPreview, options) {
20188
20411
  }
20189
20412
 
20190
20413
  // src/openai-usage.ts
20191
- var OPENAI_PRICING_SOURCE_URL = "https://developers.openai.com/api/docs/models/gpt-5.4/";
20192
- var OPENAI_PRICING_EFFECTIVE_DATE = "2026-05-19";
20414
+ var OPENAI_GPT_5_4_PRICING_SOURCE_URL = "https://developers.openai.com/api/docs/models/gpt-5.4/";
20415
+ var OPENAI_GPT_5_6_PRICING_SOURCE_URL = "https://developers.openai.com/api/docs/pricing";
20416
+ var OPENAI_LONG_CONTEXT_THRESHOLD_TOKENS = 272e3;
20193
20417
  var OPENAI_MODEL_PRICING_USD_PER_MILLION = {
20194
20418
  "gpt-5.4": {
20195
20419
  provider: "openai",
@@ -20198,8 +20422,26 @@ var OPENAI_MODEL_PRICING_USD_PER_MILLION = {
20198
20422
  inputUsdPerMillion: 2.5,
20199
20423
  cachedInputUsdPerMillion: 0.25,
20200
20424
  outputUsdPerMillion: 15,
20201
- sourceUrl: OPENAI_PRICING_SOURCE_URL,
20202
- effectiveDate: OPENAI_PRICING_EFFECTIVE_DATE
20425
+ sourceUrl: OPENAI_GPT_5_4_PRICING_SOURCE_URL,
20426
+ effectiveDate: "2026-05-19"
20427
+ },
20428
+ "gpt-5.6-luna": {
20429
+ provider: "openai",
20430
+ model: "gpt-5.6-luna",
20431
+ currency: "USD",
20432
+ inputUsdPerMillion: 1,
20433
+ cachedInputUsdPerMillion: 0.1,
20434
+ cacheWriteUsdPerMillion: 1.25,
20435
+ outputUsdPerMillion: 6,
20436
+ sourceUrl: OPENAI_GPT_5_6_PRICING_SOURCE_URL,
20437
+ effectiveDate: "2026-07-11",
20438
+ longContextThresholdTokens: OPENAI_LONG_CONTEXT_THRESHOLD_TOKENS,
20439
+ longContextPricing: {
20440
+ inputUsdPerMillion: 2,
20441
+ cachedInputUsdPerMillion: 0.2,
20442
+ cacheWriteUsdPerMillion: 2.5,
20443
+ outputUsdPerMillion: 9
20444
+ }
20203
20445
  }
20204
20446
  };
20205
20447
  function asRecord5(value) {
@@ -20212,8 +20454,18 @@ function numberField(record, key) {
20212
20454
  function microsPerMillion(usdPerMillion) {
20213
20455
  return Math.round(usdPerMillion * 1e6);
20214
20456
  }
20215
- function getOpenAIModelPricing(model) {
20216
- return OPENAI_MODEL_PRICING_USD_PER_MILLION[model] || null;
20457
+ function getOpenAIModelPricing(model, inputTokens = 0) {
20458
+ const pricing = OPENAI_MODEL_PRICING_USD_PER_MILLION[model];
20459
+ if (!pricing) return null;
20460
+ const threshold = pricing.longContextThresholdTokens ?? null;
20461
+ if (pricing.longContextPricing && typeof threshold === "number" && inputTokens > threshold) {
20462
+ return {
20463
+ ...pricing,
20464
+ ...pricing.longContextPricing,
20465
+ contextTier: "long"
20466
+ };
20467
+ }
20468
+ return { ...pricing, contextTier: "short" };
20217
20469
  }
20218
20470
  function normalizeOpenAIUsage(rawUsage) {
20219
20471
  const usage = asRecord5(rawUsage);
@@ -20221,6 +20473,7 @@ function normalizeOpenAIUsage(rawUsage) {
20221
20473
  return {
20222
20474
  inputTokens: 0,
20223
20475
  cachedInputTokens: 0,
20476
+ cacheWriteTokens: 0,
20224
20477
  uncachedInputTokens: 0,
20225
20478
  outputTokens: 0,
20226
20479
  reasoningTokens: 0,
@@ -20236,20 +20489,28 @@ function normalizeOpenAIUsage(rawUsage) {
20236
20489
  inputTokens,
20237
20490
  numberField(inputDetails, "cached_tokens") || numberField(inputDetails, "cached_input_tokens")
20238
20491
  );
20492
+ const cacheWriteTokens = Math.min(
20493
+ Math.max(inputTokens - cachedInputTokens, 0),
20494
+ numberField(inputDetails, "cache_write_tokens")
20495
+ );
20239
20496
  const reasoningTokens = numberField(outputDetails, "reasoning_tokens") || numberField(outputDetails, "reasoning_output_tokens");
20240
20497
  return {
20241
20498
  inputTokens,
20242
20499
  cachedInputTokens,
20243
- uncachedInputTokens: Math.max(inputTokens - cachedInputTokens, 0),
20500
+ cacheWriteTokens,
20501
+ uncachedInputTokens: Math.max(
20502
+ inputTokens - cachedInputTokens - cacheWriteTokens,
20503
+ 0
20504
+ ),
20244
20505
  outputTokens,
20245
20506
  reasoningTokens,
20246
20507
  totalTokens
20247
20508
  };
20248
20509
  }
20249
20510
  function calculateOpenAITokenSpend(model, rawUsage) {
20250
- const pricing = getOpenAIModelPricing(model);
20251
- if (!pricing) return null;
20252
20511
  const usage = normalizeOpenAIUsage(rawUsage);
20512
+ const pricing = getOpenAIModelPricing(model, usage.inputTokens);
20513
+ if (!pricing) return null;
20253
20514
  const inputPricePerMillionMicros = microsPerMillion(
20254
20515
  pricing.inputUsdPerMillion
20255
20516
  );
@@ -20259,14 +20520,19 @@ function calculateOpenAITokenSpend(model, rawUsage) {
20259
20520
  const outputPricePerMillionMicros = microsPerMillion(
20260
20521
  pricing.outputUsdPerMillion
20261
20522
  );
20523
+ const cacheWritePricePerMillionMicros = typeof pricing.cacheWriteUsdPerMillion === "number" ? microsPerMillion(pricing.cacheWriteUsdPerMillion) : null;
20524
+ const cacheWriteCostMicros = Math.round(
20525
+ usage.cacheWriteTokens * (cacheWritePricePerMillionMicros ?? inputPricePerMillionMicros) / 1e6
20526
+ );
20262
20527
  const amountMicros = Math.round(
20263
- (usage.uncachedInputTokens * inputPricePerMillionMicros + usage.cachedInputTokens * cachedInputPricePerMillionMicros + usage.outputTokens * outputPricePerMillionMicros) / 1e6
20528
+ (usage.uncachedInputTokens * inputPricePerMillionMicros + usage.cachedInputTokens * cachedInputPricePerMillionMicros + usage.cacheWriteTokens * (cacheWritePricePerMillionMicros ?? inputPricePerMillionMicros) + usage.outputTokens * outputPricePerMillionMicros) / 1e6
20264
20529
  );
20265
20530
  return {
20266
20531
  provider: "openai",
20267
20532
  model,
20268
20533
  inputTokens: usage.inputTokens,
20269
20534
  cachedInputTokens: usage.cachedInputTokens,
20535
+ cacheWriteTokens: usage.cacheWriteTokens,
20270
20536
  uncachedInputTokens: usage.uncachedInputTokens,
20271
20537
  outputTokens: usage.outputTokens,
20272
20538
  reasoningTokens: usage.reasoningTokens,
@@ -20275,7 +20541,11 @@ function calculateOpenAITokenSpend(model, rawUsage) {
20275
20541
  currency: "USD",
20276
20542
  inputPricePerMillionMicros,
20277
20543
  cachedInputPricePerMillionMicros,
20544
+ cacheWritePricePerMillionMicros,
20545
+ cacheWriteCostMicros,
20278
20546
  outputPricePerMillionMicros,
20547
+ pricingContextTier: pricing.contextTier || "short",
20548
+ longContextThresholdTokens: pricing.longContextThresholdTokens ?? null,
20279
20549
  pricingSource: pricing.sourceUrl,
20280
20550
  pricingEffectiveAt: pricing.effectiveDate,
20281
20551
  usage