@granular-software/sdk 0.4.50 → 0.4.52
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/README.md +53 -0
- package/dist/agent-evals.d.mts +2 -2
- package/dist/agent-evals.d.ts +2 -2
- package/dist/agent-evals.js +685 -210
- package/dist/agent-evals.js.map +1 -1
- package/dist/agent-evals.mjs +685 -210
- package/dist/agent-evals.mjs.map +1 -1
- package/dist/agent-harness.js +4 -2
- package/dist/agent-harness.js.map +1 -1
- package/dist/agent-harness.mjs +4 -2
- package/dist/agent-harness.mjs.map +1 -1
- package/dist/cli/index.js +754 -253
- package/dist/{client-DaYFTHG8.d.mts → client-B2OyGOHE.d.mts} +44 -13
- package/dist/{client-BbI7ThzU.d.ts → client-BMfjRJTs.d.ts} +44 -13
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +685 -210
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +685 -210
- package/dist/index.mjs.map +1 -1
- package/dist/{spend-RpJikX9w.d.mts → spend-DpRRCrAr.d.mts} +161 -5
- package/dist/{spend-RpJikX9w.d.ts → spend-DpRRCrAr.d.ts} +161 -5
- package/dist/spend.d.mts +1 -1
- package/dist/spend.d.ts +1 -1
- package/dist/spend.js +6 -1
- package/dist/spend.js.map +1 -1
- package/dist/spend.mjs +6 -1
- package/dist/spend.mjs.map +1 -1
- package/package.json +1 -1
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,
|
|
48
|
-
if (
|
|
49
|
-
for (let key of __getOwnPropNames(
|
|
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: () =>
|
|
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
|
|
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
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
if (
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
4284
|
-
|
|
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) =>
|
|
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
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
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: [
|
|
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
|
-
|
|
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",
|
|
@@ -14134,7 +14309,8 @@ function normalizeEnvironmentSetupSummary(setup) {
|
|
|
14134
14309
|
environmentId: String(setup.environmentId || ""),
|
|
14135
14310
|
sandboxId: String(setup.sandboxId || ""),
|
|
14136
14311
|
subjectId: String(setup.subjectId || ""),
|
|
14137
|
-
triggerReason: setup.triggerReason === "fresh_after_version_update" ? "fresh_after_version_update" : "new_environment",
|
|
14312
|
+
triggerReason: setup.triggerReason === "fresh_after_version_update" ? "fresh_after_version_update" : setup.triggerReason === "explicit_reset" ? "explicit_reset" : "new_environment",
|
|
14313
|
+
operationKey: typeof setup.operationKey === "string" ? setup.operationKey : null,
|
|
14138
14314
|
lifecycleStatus: setup.lifecycleStatus === "completed" || setup.lifecycleStatus === "failed" ? setup.lifecycleStatus : "running",
|
|
14139
14315
|
stage: typeof setup.stage === "string" ? setup.stage : null,
|
|
14140
14316
|
totalObjectsToImport: Number(setup.totalObjectsToImport || 0),
|
|
@@ -14255,7 +14431,7 @@ var Environment = class _Environment {
|
|
|
14255
14431
|
}
|
|
14256
14432
|
get sessions() {
|
|
14257
14433
|
return {
|
|
14258
|
-
list: async (options) => this.listSessions(options
|
|
14434
|
+
list: async (options = {}) => this.listSessions(options),
|
|
14259
14435
|
create: async (options) => this.createSession(options),
|
|
14260
14436
|
connect: async (sessionId, options) => this.connectSession(sessionId, options),
|
|
14261
14437
|
reopen: async (sessionId, options) => this.reopenSession(sessionId, options),
|
|
@@ -14390,17 +14566,12 @@ var Environment = class _Environment {
|
|
|
14390
14566
|
*/
|
|
14391
14567
|
async disconnect() {
|
|
14392
14568
|
}
|
|
14393
|
-
async listSessions(
|
|
14394
|
-
|
|
14395
|
-
|
|
14396
|
-
|
|
14397
|
-
|
|
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 });
|
|
14569
|
+
async listSessions(optionsOrStatus = {}) {
|
|
14570
|
+
const options = typeof optionsOrStatus === "string" ? { status: optionsOrStatus } : optionsOrStatus;
|
|
14571
|
+
return this.granular.listSessions({
|
|
14572
|
+
...options,
|
|
14573
|
+
environmentId: this.environmentId
|
|
14574
|
+
});
|
|
14404
14575
|
}
|
|
14405
14576
|
async getUserEnvironmentState(options = {}) {
|
|
14406
14577
|
return this.granular.getUserEnvironmentState({
|
|
@@ -14418,6 +14589,7 @@ var Environment = class _Environment {
|
|
|
14418
14589
|
return this.granular.createSession({
|
|
14419
14590
|
environmentId: this.environmentId,
|
|
14420
14591
|
clientId: options?.clientId,
|
|
14592
|
+
sessionScope: options?.sessionScope,
|
|
14421
14593
|
initialHeap: options?.initialHeap
|
|
14422
14594
|
});
|
|
14423
14595
|
}
|
|
@@ -15420,6 +15592,7 @@ var Environment = class _Environment {
|
|
|
15420
15592
|
records: recordsToImport,
|
|
15421
15593
|
batchSize: options.batchSize,
|
|
15422
15594
|
setupRunId: options.setupRunId,
|
|
15595
|
+
operationKey: options.operationKey,
|
|
15423
15596
|
writeMode: options.writeMode
|
|
15424
15597
|
})
|
|
15425
15598
|
}
|
|
@@ -15980,7 +16153,7 @@ var EnvironmentSession = class extends Session {
|
|
|
15980
16153
|
* Close only the socket transport without sending `client.goodbye`.
|
|
15981
16154
|
*/
|
|
15982
16155
|
disconnectTransport() {
|
|
15983
|
-
this.client.disconnect();
|
|
16156
|
+
this.client.disconnect({ reason: "Transport detach" });
|
|
15984
16157
|
}
|
|
15985
16158
|
/**
|
|
15986
16159
|
* Backwards-compatible alias for `disconnect()`.
|
|
@@ -16224,6 +16397,107 @@ var Granular = class _Granular {
|
|
|
16224
16397
|
await this.maybeRunEnvironmentImporter(resolved, environment);
|
|
16225
16398
|
return environment;
|
|
16226
16399
|
}
|
|
16400
|
+
/**
|
|
16401
|
+
* Read the active environment selected by Granular for an already-recorded
|
|
16402
|
+
* external user. This is intentionally read-only: browser/login code must
|
|
16403
|
+
* not create subjects or environments as a side effect.
|
|
16404
|
+
*/
|
|
16405
|
+
async getActiveEnvironmentForUser(options) {
|
|
16406
|
+
const sandboxId = options.sandboxId.trim();
|
|
16407
|
+
const tagName = options.tag.trim();
|
|
16408
|
+
const userId = options.userId.trim();
|
|
16409
|
+
if (!sandboxId || !tagName || !userId) {
|
|
16410
|
+
throw new Error(
|
|
16411
|
+
"getActiveEnvironmentForUser() requires sandboxId, tag, and userId."
|
|
16412
|
+
);
|
|
16413
|
+
}
|
|
16414
|
+
const subjects = await this.request(
|
|
16415
|
+
`/control/subjects?identityId=${encodeURIComponent(userId)}`
|
|
16416
|
+
);
|
|
16417
|
+
const subject = (subjects.items || []).find(
|
|
16418
|
+
(item) => item.identityId === userId || item.userId === userId
|
|
16419
|
+
);
|
|
16420
|
+
if (!subject?.subjectId && !subject?.granularId) {
|
|
16421
|
+
return null;
|
|
16422
|
+
}
|
|
16423
|
+
const subjectId = subject.subjectId || subject.granularId;
|
|
16424
|
+
const tags = await this.request(
|
|
16425
|
+
`/control/sandboxes/${encodeURIComponent(sandboxId)}/tags`
|
|
16426
|
+
);
|
|
16427
|
+
const tag = (tags.items || []).find(
|
|
16428
|
+
(item) => item?.name === tagName
|
|
16429
|
+
);
|
|
16430
|
+
if (!tag) return null;
|
|
16431
|
+
const query = new URLSearchParams({
|
|
16432
|
+
tagId: tag.tagId,
|
|
16433
|
+
slot: options.slot?.trim() || "default"
|
|
16434
|
+
});
|
|
16435
|
+
try {
|
|
16436
|
+
const payload = await this.request(
|
|
16437
|
+
`/control/sandboxes/${encodeURIComponent(sandboxId)}/subjects/${encodeURIComponent(subjectId)}/active-environment?${query.toString()}`
|
|
16438
|
+
);
|
|
16439
|
+
return payload.environment ? this.bindEnvironmentHandle(
|
|
16440
|
+
normalizeEnvironmentData(payload.environment)
|
|
16441
|
+
) : null;
|
|
16442
|
+
} catch (error) {
|
|
16443
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
16444
|
+
if (message.includes("404") || message.includes("not found")) {
|
|
16445
|
+
return null;
|
|
16446
|
+
}
|
|
16447
|
+
throw error;
|
|
16448
|
+
}
|
|
16449
|
+
}
|
|
16450
|
+
/**
|
|
16451
|
+
* Register one reviewed, pre-existing environment as the active workspace
|
|
16452
|
+
* for an external user. This is for a controlled migration only: it does
|
|
16453
|
+
* not create an environment and it does not run an importer.
|
|
16454
|
+
*/
|
|
16455
|
+
async adoptEnvironmentForUser(options) {
|
|
16456
|
+
const sandboxId = options.sandboxId.trim();
|
|
16457
|
+
const tagName = options.tag.trim();
|
|
16458
|
+
const userId = options.userId.trim();
|
|
16459
|
+
const environmentId = options.environmentId.trim();
|
|
16460
|
+
if (!sandboxId || !tagName || !userId || !environmentId) {
|
|
16461
|
+
throw new Error(
|
|
16462
|
+
"adoptEnvironmentForUser() requires sandboxId, tag, userId, and environmentId."
|
|
16463
|
+
);
|
|
16464
|
+
}
|
|
16465
|
+
const subjects = await this.request(
|
|
16466
|
+
`/control/subjects?identityId=${encodeURIComponent(userId)}`
|
|
16467
|
+
);
|
|
16468
|
+
const subject = (subjects.items || []).find(
|
|
16469
|
+
(item) => item.identityId === userId || item.userId === userId
|
|
16470
|
+
);
|
|
16471
|
+
if (!subject?.subjectId && !subject?.granularId) {
|
|
16472
|
+
throw new Error(`No Granular subject exists for user ${userId}.`);
|
|
16473
|
+
}
|
|
16474
|
+
const tags = await this.request(`/control/sandboxes/${encodeURIComponent(sandboxId)}/tags`);
|
|
16475
|
+
const tag = (tags.items || []).find(
|
|
16476
|
+
(item) => item?.name === tagName
|
|
16477
|
+
);
|
|
16478
|
+
if (!tag) {
|
|
16479
|
+
throw new Error(`Tag ${tagName} was not found in sandbox ${sandboxId}.`);
|
|
16480
|
+
}
|
|
16481
|
+
const payload = await this.request(
|
|
16482
|
+
"/control/environment-activations/adopt",
|
|
16483
|
+
{
|
|
16484
|
+
method: "POST",
|
|
16485
|
+
body: JSON.stringify({
|
|
16486
|
+
environmentId,
|
|
16487
|
+
subjectId: subject.subjectId || subject.granularId,
|
|
16488
|
+
tagId: tag.tagId,
|
|
16489
|
+
slot: options.slot?.trim() || "default",
|
|
16490
|
+
confirmExistingData: true
|
|
16491
|
+
})
|
|
16492
|
+
}
|
|
16493
|
+
);
|
|
16494
|
+
if (!payload.environment) {
|
|
16495
|
+
throw new Error("Granular did not return the adopted environment.");
|
|
16496
|
+
}
|
|
16497
|
+
return this.bindEnvironmentHandle(
|
|
16498
|
+
normalizeEnvironmentData(payload.environment)
|
|
16499
|
+
);
|
|
16500
|
+
}
|
|
16227
16501
|
/**
|
|
16228
16502
|
* Deprecated compatibility alias for `openEnvironment()`.
|
|
16229
16503
|
*
|
|
@@ -16255,7 +16529,9 @@ var Granular = class _Granular {
|
|
|
16255
16529
|
requestedOntology,
|
|
16256
16530
|
sandboxId: environmentData.sandboxId,
|
|
16257
16531
|
subjectId: environmentData.subjectId,
|
|
16258
|
-
|
|
16532
|
+
externalUserId: environmentData.subjectId,
|
|
16533
|
+
setupTriggerReason: options.reason || "new_environment",
|
|
16534
|
+
setupOperationKey: options.operationKey
|
|
16259
16535
|
},
|
|
16260
16536
|
environment
|
|
16261
16537
|
);
|
|
@@ -16267,20 +16543,17 @@ var Granular = class _Granular {
|
|
|
16267
16543
|
}
|
|
16268
16544
|
return tag;
|
|
16269
16545
|
}
|
|
16270
|
-
buildManagedEnvironmentName(tag, versionId) {
|
|
16271
|
-
|
|
16546
|
+
buildManagedEnvironmentName(tag, versionId, resetKey) {
|
|
16547
|
+
if (!resetKey) {
|
|
16548
|
+
return `__sdk__${tag}__${versionId}__tracked`;
|
|
16549
|
+
}
|
|
16550
|
+
const safeResetKey = resetKey.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 80);
|
|
16551
|
+
return `__sdk__${tag}__${versionId}__reset__${safeResetKey}`;
|
|
16272
16552
|
}
|
|
16273
16553
|
isManagedEnvironmentName(environment, tagName) {
|
|
16274
16554
|
const name = environment.environment || environment.envName || "";
|
|
16275
16555
|
return name.startsWith(`__sdk__${tagName}__`);
|
|
16276
16556
|
}
|
|
16277
|
-
isPinnedToVersion(environment, versionId) {
|
|
16278
|
-
return environment.buildPolicy.mode === "pinned" && (environment.versionId === versionId || environment.buildPolicy.versionId === versionId || environment.buildPolicy.buildId === versionId);
|
|
16279
|
-
}
|
|
16280
|
-
matchesTagTrackedEnvironment(environment, tagName, tagId) {
|
|
16281
|
-
const environmentTagName = environment.tag?.name || environment.buildPolicy.tagName || null;
|
|
16282
|
-
return environment.tagId === tagId || environmentTagName === tagName || environment.environment === tagName || environment.envName === tagName || environment.environment === this.buildManagedEnvironmentName(tagName, environment.versionId) || environment.envName === this.buildManagedEnvironmentName(tagName, environment.versionId);
|
|
16283
|
-
}
|
|
16284
16557
|
sortEnvironmentsByRecency(environments) {
|
|
16285
16558
|
return [...environments].sort(
|
|
16286
16559
|
(left, right) => right.updatedAt - left.updatedAt
|
|
@@ -16330,61 +16603,187 @@ var Granular = class _Granular {
|
|
|
16330
16603
|
`Tag "${tagName}" does not currently point to a build/version.`
|
|
16331
16604
|
);
|
|
16332
16605
|
}
|
|
16606
|
+
const slot = options.slot?.trim() || "default";
|
|
16607
|
+
const resetKey = options.resetKey?.trim() || void 0;
|
|
16608
|
+
const resolveActive = async (operationKey) => {
|
|
16609
|
+
const query = new URLSearchParams({ tagId: tag.tagId, slot });
|
|
16610
|
+
if (operationKey) query.set("operationKey", operationKey);
|
|
16611
|
+
try {
|
|
16612
|
+
const payload = await this.request(
|
|
16613
|
+
`/control/sandboxes/${encodeURIComponent(sandbox.sandboxId)}/subjects/${encodeURIComponent(user.granularId)}/active-environment?${query.toString()}`
|
|
16614
|
+
);
|
|
16615
|
+
return payload.environment ? normalizeEnvironmentData(payload.environment) : null;
|
|
16616
|
+
} catch (error) {
|
|
16617
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
16618
|
+
if (message.includes("404") || message.includes("not found")) {
|
|
16619
|
+
return null;
|
|
16620
|
+
}
|
|
16621
|
+
throw error;
|
|
16622
|
+
}
|
|
16623
|
+
};
|
|
16624
|
+
const activate = async (environment2) => {
|
|
16625
|
+
const payload = await this.request(
|
|
16626
|
+
"/control/environment-activations",
|
|
16627
|
+
{
|
|
16628
|
+
method: "POST",
|
|
16629
|
+
body: JSON.stringify({
|
|
16630
|
+
environmentId: environment2.environmentId,
|
|
16631
|
+
tagId: tag.tagId,
|
|
16632
|
+
slot,
|
|
16633
|
+
operationKey: resetKey,
|
|
16634
|
+
operation: resetKey ? "explicit_reset" : void 0
|
|
16635
|
+
})
|
|
16636
|
+
}
|
|
16637
|
+
);
|
|
16638
|
+
return normalizeEnvironmentData(payload.environment);
|
|
16639
|
+
};
|
|
16640
|
+
if (resetKey) {
|
|
16641
|
+
const resetEnvironment = await resolveActive(resetKey);
|
|
16642
|
+
if (resetEnvironment) {
|
|
16643
|
+
return {
|
|
16644
|
+
environment: resetEnvironment,
|
|
16645
|
+
requestedOntology: ontology,
|
|
16646
|
+
sandboxId: sandbox.sandboxId,
|
|
16647
|
+
subjectId: user.granularId,
|
|
16648
|
+
externalUserId: user.userId,
|
|
16649
|
+
// Retrying an explicit reset must also resume its durable setup run.
|
|
16650
|
+
// Otherwise a Container crash after queue submission would leave a
|
|
16651
|
+
// valid environment permanently marked as "running".
|
|
16652
|
+
setupTriggerReason: "explicit_reset",
|
|
16653
|
+
setupOperationKey: resetKey
|
|
16654
|
+
};
|
|
16655
|
+
}
|
|
16656
|
+
} else {
|
|
16657
|
+
const active = await resolveActive();
|
|
16658
|
+
if (active && (active.versionId === targetVersionId || options.createFreshIfOutdated !== true)) {
|
|
16659
|
+
return {
|
|
16660
|
+
environment: active,
|
|
16661
|
+
requestedOntology: ontology,
|
|
16662
|
+
sandboxId: sandbox.sandboxId,
|
|
16663
|
+
subjectId: user.granularId,
|
|
16664
|
+
externalUserId: user.userId
|
|
16665
|
+
};
|
|
16666
|
+
}
|
|
16667
|
+
}
|
|
16333
16668
|
const allEnvironments = await this.environments.list(sandbox.sandboxId);
|
|
16334
16669
|
const userEnvironments = allEnvironments.filter(
|
|
16335
|
-
(
|
|
16670
|
+
(environment2) => environment2.subjectId === user.granularId
|
|
16336
16671
|
);
|
|
16337
16672
|
const currentMatches = this.sortEnvironmentsByRecency(
|
|
16338
16673
|
userEnvironments.filter(
|
|
16339
|
-
(
|
|
16674
|
+
(environment2) => environment2.tagId === tag.tagId && environment2.versionId === targetVersionId
|
|
16340
16675
|
)
|
|
16341
16676
|
);
|
|
16342
|
-
if (currentMatches.length > 0) {
|
|
16677
|
+
if (!resetKey && currentMatches.length > 0) {
|
|
16678
|
+
const environment2 = await activate(currentMatches[0]);
|
|
16343
16679
|
return {
|
|
16344
|
-
environment:
|
|
16680
|
+
environment: environment2,
|
|
16345
16681
|
requestedOntology: ontology,
|
|
16346
16682
|
sandboxId: sandbox.sandboxId,
|
|
16347
|
-
subjectId: user.granularId
|
|
16683
|
+
subjectId: user.granularId,
|
|
16684
|
+
externalUserId: user.userId
|
|
16348
16685
|
};
|
|
16349
16686
|
}
|
|
16350
16687
|
const outdatedMatches = this.sortEnvironmentsByRecency(
|
|
16351
|
-
userEnvironments.filter(
|
|
16352
|
-
(environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag.tagId)
|
|
16353
|
-
)
|
|
16688
|
+
userEnvironments.filter((environment2) => environment2.tagId === tag.tagId)
|
|
16354
16689
|
);
|
|
16355
16690
|
if (outdatedMatches.length > 0 && options.createFreshIfOutdated !== true) {
|
|
16691
|
+
const environment2 = await activate(outdatedMatches[0]);
|
|
16356
16692
|
return {
|
|
16357
|
-
environment:
|
|
16693
|
+
environment: environment2,
|
|
16358
16694
|
requestedOntology: ontology,
|
|
16359
16695
|
sandboxId: sandbox.sandboxId,
|
|
16360
|
-
subjectId: user.granularId
|
|
16696
|
+
subjectId: user.granularId,
|
|
16697
|
+
externalUserId: user.userId
|
|
16361
16698
|
};
|
|
16362
16699
|
}
|
|
16700
|
+
const created = await this.environments.create(sandbox.sandboxId, {
|
|
16701
|
+
subjectId: user.granularId,
|
|
16702
|
+
environment: this.buildManagedEnvironmentName(
|
|
16703
|
+
tagName,
|
|
16704
|
+
targetVersionId,
|
|
16705
|
+
resetKey
|
|
16706
|
+
),
|
|
16707
|
+
tagId: tag.tagId,
|
|
16708
|
+
permissionProfileId: null
|
|
16709
|
+
});
|
|
16710
|
+
const environment = await activate(created);
|
|
16363
16711
|
return {
|
|
16364
|
-
environment
|
|
16365
|
-
subjectId: user.granularId,
|
|
16366
|
-
environment: this.buildManagedEnvironmentName(tagName, targetVersionId),
|
|
16367
|
-
tagId: tag.tagId,
|
|
16368
|
-
versionId: targetVersionId,
|
|
16369
|
-
permissionProfileId: null
|
|
16370
|
-
}),
|
|
16712
|
+
environment,
|
|
16371
16713
|
requestedOntology: ontology,
|
|
16372
16714
|
sandboxId: sandbox.sandboxId,
|
|
16373
16715
|
subjectId: user.granularId,
|
|
16374
|
-
|
|
16716
|
+
externalUserId: user.userId,
|
|
16717
|
+
setupTriggerReason: resetKey ? "explicit_reset" : outdatedMatches.length > 0 ? "fresh_after_version_update" : "new_environment",
|
|
16718
|
+
setupOperationKey: resetKey
|
|
16375
16719
|
};
|
|
16376
16720
|
}
|
|
16377
16721
|
/**
|
|
16378
|
-
* List
|
|
16722
|
+
* List indexed sessions using ownership filters and bounded pagination.
|
|
16723
|
+
*/
|
|
16724
|
+
async listSessions(options) {
|
|
16725
|
+
const environmentId = options.environmentId?.trim();
|
|
16726
|
+
const sandboxId = options.sandboxId?.trim();
|
|
16727
|
+
const subjectId = options.subjectId?.trim();
|
|
16728
|
+
if (!environmentId && !sandboxId && !subjectId) {
|
|
16729
|
+
throw new Error(
|
|
16730
|
+
"listSessions() requires environmentId, sandboxId, or subjectId so history cannot be scanned accidentally."
|
|
16731
|
+
);
|
|
16732
|
+
}
|
|
16733
|
+
const status = options.status || "active";
|
|
16734
|
+
const allowedStatuses = /* @__PURE__ */ new Set([
|
|
16735
|
+
"active",
|
|
16736
|
+
"closed",
|
|
16737
|
+
"expired",
|
|
16738
|
+
"failed",
|
|
16739
|
+
"timeout",
|
|
16740
|
+
"all"
|
|
16741
|
+
]);
|
|
16742
|
+
if (!allowedStatuses.has(status)) {
|
|
16743
|
+
throw new Error(`Unsupported session status: ${String(status)}`);
|
|
16744
|
+
}
|
|
16745
|
+
const limit = boundedSessionListInteger(
|
|
16746
|
+
options.limit,
|
|
16747
|
+
"limit",
|
|
16748
|
+
DEFAULT_CONVERSATION_SESSION_LIST_LIMIT,
|
|
16749
|
+
1,
|
|
16750
|
+
MAX_CONVERSATION_SESSION_LIST_LIMIT
|
|
16751
|
+
);
|
|
16752
|
+
const offset = boundedSessionListInteger(
|
|
16753
|
+
options.offset,
|
|
16754
|
+
"offset",
|
|
16755
|
+
0,
|
|
16756
|
+
0,
|
|
16757
|
+
MAX_CONVERSATION_SESSION_LIST_OFFSET
|
|
16758
|
+
);
|
|
16759
|
+
const query = new URLSearchParams({
|
|
16760
|
+
limit: String(limit),
|
|
16761
|
+
offset: String(offset)
|
|
16762
|
+
});
|
|
16763
|
+
if (environmentId) query.set("environmentId", environmentId);
|
|
16764
|
+
if (sandboxId) query.set("sandboxId", sandboxId);
|
|
16765
|
+
if (subjectId) query.set("userId", subjectId);
|
|
16766
|
+
if (options.sessionScope?.trim()) {
|
|
16767
|
+
query.set("sessionScope", options.sessionScope.trim());
|
|
16768
|
+
}
|
|
16769
|
+
if (status !== "all") query.set("status", status);
|
|
16770
|
+
const res = await this.request(
|
|
16771
|
+
`/control/sessions?${query.toString()}`
|
|
16772
|
+
);
|
|
16773
|
+
const items = Array.isArray(res.items) ? res.items : [];
|
|
16774
|
+
return items.map((row) => this.normalizeConversationSession(row));
|
|
16775
|
+
}
|
|
16776
|
+
/**
|
|
16777
|
+
* List active (open) sessions for an environment.
|
|
16379
16778
|
*/
|
|
16380
16779
|
async listOpenSessions(filters) {
|
|
16381
|
-
return this.
|
|
16780
|
+
return this.listSessions({ ...filters, status: "active" });
|
|
16382
16781
|
}
|
|
16383
16782
|
/**
|
|
16384
16783
|
* List closed sessions for an environment (conversations that have disconnected).
|
|
16385
16784
|
*/
|
|
16386
16785
|
async listClosedSessions(filters) {
|
|
16387
|
-
return this.
|
|
16786
|
+
return this.listSessions({ ...filters, status: "closed" });
|
|
16388
16787
|
}
|
|
16389
16788
|
async getUserEnvironmentState(options) {
|
|
16390
16789
|
const query = new URLSearchParams({
|
|
@@ -16419,14 +16818,6 @@ var Granular = class _Granular {
|
|
|
16419
16818
|
});
|
|
16420
16819
|
return result.readAtBySessionId || {};
|
|
16421
16820
|
}
|
|
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
16821
|
normalizeConversationSession(row) {
|
|
16431
16822
|
const sessionId = String(row.sessionId ?? row.session_id ?? "");
|
|
16432
16823
|
const environmentId = String(row.environmentId ?? row.environment_id ?? "");
|
|
@@ -16485,6 +16876,7 @@ var Granular = class _Granular {
|
|
|
16485
16876
|
*/
|
|
16486
16877
|
async createSession(options) {
|
|
16487
16878
|
const clientId = options.clientId || `client_${Date.now()}`;
|
|
16879
|
+
const sessionScope = options.sessionScope?.trim() || void 0;
|
|
16488
16880
|
await this.activateEnvironment(options.environmentId);
|
|
16489
16881
|
const envData = await this.environments.get(options.environmentId);
|
|
16490
16882
|
const environment = this.bindEnvironmentHandle(envData);
|
|
@@ -16493,6 +16885,8 @@ var Granular = class _Granular {
|
|
|
16493
16885
|
body: JSON.stringify({
|
|
16494
16886
|
environmentId: options.environmentId,
|
|
16495
16887
|
clientId,
|
|
16888
|
+
sessionScope,
|
|
16889
|
+
capabilities: sessionScope ? { sessionScope } : void 0,
|
|
16496
16890
|
initialHeap: options.initialHeap
|
|
16497
16891
|
})
|
|
16498
16892
|
});
|
|
@@ -16600,11 +16994,24 @@ var Granular = class _Granular {
|
|
|
16600
16994
|
{
|
|
16601
16995
|
method: "POST",
|
|
16602
16996
|
body: JSON.stringify({
|
|
16603
|
-
triggerReason: resolved.setupTriggerReason
|
|
16997
|
+
triggerReason: resolved.setupTriggerReason,
|
|
16998
|
+
operationKey: resolved.setupOperationKey
|
|
16604
16999
|
})
|
|
16605
17000
|
}
|
|
16606
17001
|
);
|
|
16607
17002
|
const setupRunId = setupRun.setupRunId;
|
|
17003
|
+
let claim = null;
|
|
17004
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
17005
|
+
claim = await this.request(
|
|
17006
|
+
`/control/environment-setup-runs/${setupRunId}/importer-claim`,
|
|
17007
|
+
{ method: "POST", body: JSON.stringify({}) }
|
|
17008
|
+
);
|
|
17009
|
+
if (claim.action !== "busy") break;
|
|
17010
|
+
await sleep(Math.min(3e4, Math.max(250, claim.retryAfterMs || 1e3)));
|
|
17011
|
+
}
|
|
17012
|
+
if (!claim) {
|
|
17013
|
+
throw new Error(`Unable to claim environment setup run ${setupRunId}.`);
|
|
17014
|
+
}
|
|
16608
17015
|
const updateSetupRun = async (patch) => {
|
|
16609
17016
|
await this.request(
|
|
16610
17017
|
`/control/environment-setup-runs/${setupRunId}`,
|
|
@@ -16614,10 +17021,26 @@ var Granular = class _Granular {
|
|
|
16614
17021
|
}
|
|
16615
17022
|
);
|
|
16616
17023
|
};
|
|
17024
|
+
if (claim.action === "submitted") {
|
|
17025
|
+
const completedSetupRun = await this.request(
|
|
17026
|
+
`/control/environment-setup-runs/${setupRunId}`,
|
|
17027
|
+
{ method: "PATCH", body: JSON.stringify({ markHookCompleted: true }) }
|
|
17028
|
+
);
|
|
17029
|
+
const refreshedEnvironment = await this.environments.get(
|
|
17030
|
+
environment.environmentId
|
|
17031
|
+
);
|
|
17032
|
+
environment.syncEnvironmentData(refreshedEnvironment);
|
|
17033
|
+
return completedSetupRun;
|
|
17034
|
+
}
|
|
17035
|
+
if (claim.action === "busy" || claim.action === "terminal") {
|
|
17036
|
+
return claim.summary;
|
|
17037
|
+
}
|
|
17038
|
+
let importSequence = 0;
|
|
16617
17039
|
const importerContext = {
|
|
16618
17040
|
environmentId: environment.environmentId,
|
|
16619
17041
|
sandboxId: environment.sandboxId,
|
|
16620
17042
|
subjectId: environment.subjectId,
|
|
17043
|
+
externalUserId: resolved.externalUserId,
|
|
16621
17044
|
reason: resolved.setupTriggerReason,
|
|
16622
17045
|
incrementTotalObjectsToImportCount: async (n) => {
|
|
16623
17046
|
const safeIncrement = Math.max(0, Math.trunc(n));
|
|
@@ -16634,7 +17057,10 @@ var Granular = class _Granular {
|
|
|
16634
17057
|
importRecords: async (records, options) => environment.enqueueRecordImport(records, {
|
|
16635
17058
|
batchSize: options?.batchSize,
|
|
16636
17059
|
writeMode: options?.writeMode,
|
|
16637
|
-
setupRunId
|
|
17060
|
+
setupRunId,
|
|
17061
|
+
// Sequence is deterministic for a retry of one importer hook. It
|
|
17062
|
+
// prevents a Container restart from creating a second queue import.
|
|
17063
|
+
operationKey: `${setupRunId}:import:${importSequence++}`
|
|
16638
17064
|
})
|
|
16639
17065
|
};
|
|
16640
17066
|
try {
|
|
@@ -19684,6 +20110,7 @@ function buildGranularAgentSystemPrompt(input) {
|
|
|
19684
20110
|
- \`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
20111
|
- 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
20112
|
- 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()\`.
|
|
20113
|
+
- 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
20114
|
- 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
20115
|
- 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
20116
|
- 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 +20381,12 @@ ${actionIndex}
|
|
|
19954
20381
|
- 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
20382
|
- 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
20383
|
- 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
|
-
-
|
|
20384
|
+
- 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.
|
|
20385
|
+
- 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
20386
|
- 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
20387
|
- 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
20388
|
- 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
|
|
20389
|
+
- 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
20390
|
- 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
20391
|
- 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
20392
|
- 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 +20616,9 @@ function buildContinuationInstructionFromTemplate(resultPreview, options) {
|
|
|
20188
20616
|
}
|
|
20189
20617
|
|
|
20190
20618
|
// src/openai-usage.ts
|
|
20191
|
-
var
|
|
20192
|
-
var
|
|
20619
|
+
var OPENAI_GPT_5_4_PRICING_SOURCE_URL = "https://developers.openai.com/api/docs/models/gpt-5.4/";
|
|
20620
|
+
var OPENAI_GPT_5_6_PRICING_SOURCE_URL = "https://developers.openai.com/api/docs/pricing";
|
|
20621
|
+
var OPENAI_LONG_CONTEXT_THRESHOLD_TOKENS = 272e3;
|
|
20193
20622
|
var OPENAI_MODEL_PRICING_USD_PER_MILLION = {
|
|
20194
20623
|
"gpt-5.4": {
|
|
20195
20624
|
provider: "openai",
|
|
@@ -20198,8 +20627,26 @@ var OPENAI_MODEL_PRICING_USD_PER_MILLION = {
|
|
|
20198
20627
|
inputUsdPerMillion: 2.5,
|
|
20199
20628
|
cachedInputUsdPerMillion: 0.25,
|
|
20200
20629
|
outputUsdPerMillion: 15,
|
|
20201
|
-
sourceUrl:
|
|
20202
|
-
effectiveDate:
|
|
20630
|
+
sourceUrl: OPENAI_GPT_5_4_PRICING_SOURCE_URL,
|
|
20631
|
+
effectiveDate: "2026-05-19"
|
|
20632
|
+
},
|
|
20633
|
+
"gpt-5.6-luna": {
|
|
20634
|
+
provider: "openai",
|
|
20635
|
+
model: "gpt-5.6-luna",
|
|
20636
|
+
currency: "USD",
|
|
20637
|
+
inputUsdPerMillion: 1,
|
|
20638
|
+
cachedInputUsdPerMillion: 0.1,
|
|
20639
|
+
cacheWriteUsdPerMillion: 1.25,
|
|
20640
|
+
outputUsdPerMillion: 6,
|
|
20641
|
+
sourceUrl: OPENAI_GPT_5_6_PRICING_SOURCE_URL,
|
|
20642
|
+
effectiveDate: "2026-07-11",
|
|
20643
|
+
longContextThresholdTokens: OPENAI_LONG_CONTEXT_THRESHOLD_TOKENS,
|
|
20644
|
+
longContextPricing: {
|
|
20645
|
+
inputUsdPerMillion: 2,
|
|
20646
|
+
cachedInputUsdPerMillion: 0.2,
|
|
20647
|
+
cacheWriteUsdPerMillion: 2.5,
|
|
20648
|
+
outputUsdPerMillion: 9
|
|
20649
|
+
}
|
|
20203
20650
|
}
|
|
20204
20651
|
};
|
|
20205
20652
|
function asRecord5(value) {
|
|
@@ -20212,8 +20659,18 @@ function numberField(record, key) {
|
|
|
20212
20659
|
function microsPerMillion(usdPerMillion) {
|
|
20213
20660
|
return Math.round(usdPerMillion * 1e6);
|
|
20214
20661
|
}
|
|
20215
|
-
function getOpenAIModelPricing(model) {
|
|
20216
|
-
|
|
20662
|
+
function getOpenAIModelPricing(model, inputTokens = 0) {
|
|
20663
|
+
const pricing = OPENAI_MODEL_PRICING_USD_PER_MILLION[model];
|
|
20664
|
+
if (!pricing) return null;
|
|
20665
|
+
const threshold = pricing.longContextThresholdTokens ?? null;
|
|
20666
|
+
if (pricing.longContextPricing && typeof threshold === "number" && inputTokens > threshold) {
|
|
20667
|
+
return {
|
|
20668
|
+
...pricing,
|
|
20669
|
+
...pricing.longContextPricing,
|
|
20670
|
+
contextTier: "long"
|
|
20671
|
+
};
|
|
20672
|
+
}
|
|
20673
|
+
return { ...pricing, contextTier: "short" };
|
|
20217
20674
|
}
|
|
20218
20675
|
function normalizeOpenAIUsage(rawUsage) {
|
|
20219
20676
|
const usage = asRecord5(rawUsage);
|
|
@@ -20221,6 +20678,7 @@ function normalizeOpenAIUsage(rawUsage) {
|
|
|
20221
20678
|
return {
|
|
20222
20679
|
inputTokens: 0,
|
|
20223
20680
|
cachedInputTokens: 0,
|
|
20681
|
+
cacheWriteTokens: 0,
|
|
20224
20682
|
uncachedInputTokens: 0,
|
|
20225
20683
|
outputTokens: 0,
|
|
20226
20684
|
reasoningTokens: 0,
|
|
@@ -20236,20 +20694,28 @@ function normalizeOpenAIUsage(rawUsage) {
|
|
|
20236
20694
|
inputTokens,
|
|
20237
20695
|
numberField(inputDetails, "cached_tokens") || numberField(inputDetails, "cached_input_tokens")
|
|
20238
20696
|
);
|
|
20697
|
+
const cacheWriteTokens = Math.min(
|
|
20698
|
+
Math.max(inputTokens - cachedInputTokens, 0),
|
|
20699
|
+
numberField(inputDetails, "cache_write_tokens")
|
|
20700
|
+
);
|
|
20239
20701
|
const reasoningTokens = numberField(outputDetails, "reasoning_tokens") || numberField(outputDetails, "reasoning_output_tokens");
|
|
20240
20702
|
return {
|
|
20241
20703
|
inputTokens,
|
|
20242
20704
|
cachedInputTokens,
|
|
20243
|
-
|
|
20705
|
+
cacheWriteTokens,
|
|
20706
|
+
uncachedInputTokens: Math.max(
|
|
20707
|
+
inputTokens - cachedInputTokens - cacheWriteTokens,
|
|
20708
|
+
0
|
|
20709
|
+
),
|
|
20244
20710
|
outputTokens,
|
|
20245
20711
|
reasoningTokens,
|
|
20246
20712
|
totalTokens
|
|
20247
20713
|
};
|
|
20248
20714
|
}
|
|
20249
20715
|
function calculateOpenAITokenSpend(model, rawUsage) {
|
|
20250
|
-
const pricing = getOpenAIModelPricing(model);
|
|
20251
|
-
if (!pricing) return null;
|
|
20252
20716
|
const usage = normalizeOpenAIUsage(rawUsage);
|
|
20717
|
+
const pricing = getOpenAIModelPricing(model, usage.inputTokens);
|
|
20718
|
+
if (!pricing) return null;
|
|
20253
20719
|
const inputPricePerMillionMicros = microsPerMillion(
|
|
20254
20720
|
pricing.inputUsdPerMillion
|
|
20255
20721
|
);
|
|
@@ -20259,14 +20725,19 @@ function calculateOpenAITokenSpend(model, rawUsage) {
|
|
|
20259
20725
|
const outputPricePerMillionMicros = microsPerMillion(
|
|
20260
20726
|
pricing.outputUsdPerMillion
|
|
20261
20727
|
);
|
|
20728
|
+
const cacheWritePricePerMillionMicros = typeof pricing.cacheWriteUsdPerMillion === "number" ? microsPerMillion(pricing.cacheWriteUsdPerMillion) : null;
|
|
20729
|
+
const cacheWriteCostMicros = Math.round(
|
|
20730
|
+
usage.cacheWriteTokens * (cacheWritePricePerMillionMicros ?? inputPricePerMillionMicros) / 1e6
|
|
20731
|
+
);
|
|
20262
20732
|
const amountMicros = Math.round(
|
|
20263
|
-
(usage.uncachedInputTokens * inputPricePerMillionMicros + usage.cachedInputTokens * cachedInputPricePerMillionMicros + usage.outputTokens * outputPricePerMillionMicros) / 1e6
|
|
20733
|
+
(usage.uncachedInputTokens * inputPricePerMillionMicros + usage.cachedInputTokens * cachedInputPricePerMillionMicros + usage.cacheWriteTokens * (cacheWritePricePerMillionMicros ?? inputPricePerMillionMicros) + usage.outputTokens * outputPricePerMillionMicros) / 1e6
|
|
20264
20734
|
);
|
|
20265
20735
|
return {
|
|
20266
20736
|
provider: "openai",
|
|
20267
20737
|
model,
|
|
20268
20738
|
inputTokens: usage.inputTokens,
|
|
20269
20739
|
cachedInputTokens: usage.cachedInputTokens,
|
|
20740
|
+
cacheWriteTokens: usage.cacheWriteTokens,
|
|
20270
20741
|
uncachedInputTokens: usage.uncachedInputTokens,
|
|
20271
20742
|
outputTokens: usage.outputTokens,
|
|
20272
20743
|
reasoningTokens: usage.reasoningTokens,
|
|
@@ -20275,7 +20746,11 @@ function calculateOpenAITokenSpend(model, rawUsage) {
|
|
|
20275
20746
|
currency: "USD",
|
|
20276
20747
|
inputPricePerMillionMicros,
|
|
20277
20748
|
cachedInputPricePerMillionMicros,
|
|
20749
|
+
cacheWritePricePerMillionMicros,
|
|
20750
|
+
cacheWriteCostMicros,
|
|
20278
20751
|
outputPricePerMillionMicros,
|
|
20752
|
+
pricingContextTier: pricing.contextTier || "short",
|
|
20753
|
+
longContextThresholdTokens: pricing.longContextThresholdTokens ?? null,
|
|
20279
20754
|
pricingSource: pricing.sourceUrl,
|
|
20280
20755
|
pricingEffectiveAt: pricing.effectiveDate,
|
|
20281
20756
|
usage
|