@granular-software/sdk 0.4.37 → 0.4.38
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/agent-evals.d.mts +16 -3
- package/dist/agent-evals.d.ts +16 -3
- package/dist/agent-evals.js +341 -105
- package/dist/agent-evals.js.map +1 -1
- package/dist/agent-evals.mjs +340 -105
- package/dist/agent-evals.mjs.map +1 -1
- package/dist/cli/index.js +129 -3
- package/dist/client-BNbWA9jQ.d.ts +1064 -0
- package/dist/client-HDJgcJC5.d.mts +1064 -0
- package/dist/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +231 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +225 -4
- package/dist/index.mjs.map +1 -1
- package/dist/spend-rzS1rlFr.d.mts +1559 -0
- package/dist/spend-rzS1rlFr.d.ts +1559 -0
- package/dist/spend.d.mts +2 -0
- package/dist/spend.d.ts +2 -0
- package/dist/spend.js +111 -0
- package/dist/spend.js.map +1 -0
- package/dist/spend.mjs +107 -0
- package/dist/spend.mjs.map +1 -0
- package/package.json +7 -1
- package/dist/client-eE9nTfvp.d.mts +0 -2483
- package/dist/client-eE9nTfvp.d.ts +0 -2483
package/dist/agent-evals.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { writeFile, mkdir } from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import OpenAI from 'openai';
|
|
3
4
|
import * as Automerge from '@automerge/automerge';
|
|
4
5
|
|
|
5
6
|
var __create = Object.create;
|
|
@@ -4618,6 +4619,7 @@ function withPromptTranscriptTimeout(promise) {
|
|
|
4618
4619
|
var Session = class {
|
|
4619
4620
|
client;
|
|
4620
4621
|
clientId;
|
|
4622
|
+
initialQuota;
|
|
4621
4623
|
jobsMap = /* @__PURE__ */ new Map();
|
|
4622
4624
|
pendingAgentMessagesByJobId = /* @__PURE__ */ new Map();
|
|
4623
4625
|
eventListeners = /* @__PURE__ */ new Map();
|
|
@@ -4633,9 +4635,10 @@ var Session = class {
|
|
|
4633
4635
|
promptCache = /* @__PURE__ */ new Map();
|
|
4634
4636
|
/** Prompt ids locally answered before the document sync catches up. */
|
|
4635
4637
|
hiddenPromptIds = /* @__PURE__ */ new Set();
|
|
4636
|
-
constructor(client, clientId) {
|
|
4638
|
+
constructor(client, clientId, options = {}) {
|
|
4637
4639
|
this.client = client;
|
|
4638
4640
|
this.clientId = clientId || `client_${Date.now()}`;
|
|
4641
|
+
this.initialQuota = options.initialQuota || null;
|
|
4639
4642
|
this.setupEventHandlers();
|
|
4640
4643
|
this.setupToolInvokeHandler();
|
|
4641
4644
|
}
|
|
@@ -4684,6 +4687,16 @@ var Session = class {
|
|
|
4684
4687
|
get document() {
|
|
4685
4688
|
return this.client.doc;
|
|
4686
4689
|
}
|
|
4690
|
+
get quota() {
|
|
4691
|
+
return this.getQuota();
|
|
4692
|
+
}
|
|
4693
|
+
getQuota() {
|
|
4694
|
+
const quota = this.client.doc.billing?.quota;
|
|
4695
|
+
if (quota && typeof quota === "object") {
|
|
4696
|
+
return quota;
|
|
4697
|
+
}
|
|
4698
|
+
return this.initialQuota;
|
|
4699
|
+
}
|
|
4687
4700
|
get sessionId() {
|
|
4688
4701
|
return this.client.currentSessionId;
|
|
4689
4702
|
}
|
|
@@ -11306,6 +11319,110 @@ async function invokeRegisteredEffect(effectMap, request) {
|
|
|
11306
11319
|
return resolved.handler(request.input, context);
|
|
11307
11320
|
}
|
|
11308
11321
|
|
|
11322
|
+
// src/spend.ts
|
|
11323
|
+
function toGranularHttpBase(apiUrl) {
|
|
11324
|
+
const url = new URL(apiUrl);
|
|
11325
|
+
if (url.protocol === "ws:") {
|
|
11326
|
+
url.protocol = "http:";
|
|
11327
|
+
} else if (url.protocol === "wss:") {
|
|
11328
|
+
url.protocol = "https:";
|
|
11329
|
+
}
|
|
11330
|
+
url.pathname = url.pathname.replace(/\/ws\/connect$/, "").replace(/\/ws$/, "");
|
|
11331
|
+
if (!url.pathname || url.pathname === "/") {
|
|
11332
|
+
url.pathname = "/granular";
|
|
11333
|
+
}
|
|
11334
|
+
url.search = "";
|
|
11335
|
+
url.hash = "";
|
|
11336
|
+
return url.toString().replace(/\/$/, "");
|
|
11337
|
+
}
|
|
11338
|
+
function cleanIdPart(value) {
|
|
11339
|
+
return value.replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
|
|
11340
|
+
}
|
|
11341
|
+
function buildOpenAISpendEventId(usage, context = {}) {
|
|
11342
|
+
const requestId = usage.requestId?.trim();
|
|
11343
|
+
if (!requestId) return void 0;
|
|
11344
|
+
const scope = context.sessionId || context.environmentId || context.subjectId || context.sandboxId || "global";
|
|
11345
|
+
return ["spend", "openai", scope, requestId].map(cleanIdPart).join("_");
|
|
11346
|
+
}
|
|
11347
|
+
function pricingEffectiveAtSeconds(value) {
|
|
11348
|
+
if (!value) return null;
|
|
11349
|
+
const parsed = Date.parse(value);
|
|
11350
|
+
return Number.isFinite(parsed) ? Math.floor(parsed / 1e3) : null;
|
|
11351
|
+
}
|
|
11352
|
+
function compactContext(context) {
|
|
11353
|
+
return Object.fromEntries(
|
|
11354
|
+
Object.entries(context).filter(
|
|
11355
|
+
([, value]) => value != null && value !== ""
|
|
11356
|
+
)
|
|
11357
|
+
);
|
|
11358
|
+
}
|
|
11359
|
+
function omitTenantId(context) {
|
|
11360
|
+
const scopedContext = { ...context };
|
|
11361
|
+
delete scopedContext.tenantId;
|
|
11362
|
+
return scopedContext;
|
|
11363
|
+
}
|
|
11364
|
+
async function recordOpenAIUsageSpend(options) {
|
|
11365
|
+
const usageContext = compactContext({
|
|
11366
|
+
...options.usage.usageContext || {},
|
|
11367
|
+
...options.context || {}
|
|
11368
|
+
});
|
|
11369
|
+
const context = omitTenantId(usageContext);
|
|
11370
|
+
const spendEventId = options.usage.spendEventId || buildOpenAISpendEventId(options.usage, context);
|
|
11371
|
+
const metadata = {
|
|
11372
|
+
...options.metadata || {},
|
|
11373
|
+
...options.usage.rawUsage !== void 0 ? { openaiUsage: options.usage.rawUsage } : {},
|
|
11374
|
+
usageContext: context
|
|
11375
|
+
};
|
|
11376
|
+
const response = await fetch(
|
|
11377
|
+
`${toGranularHttpBase(options.apiUrl)}/control/spend/events`,
|
|
11378
|
+
{
|
|
11379
|
+
method: "POST",
|
|
11380
|
+
cache: "no-store",
|
|
11381
|
+
headers: {
|
|
11382
|
+
Authorization: `Bearer ${options.token}`,
|
|
11383
|
+
"Content-Type": "application/json"
|
|
11384
|
+
},
|
|
11385
|
+
body: JSON.stringify({
|
|
11386
|
+
...spendEventId ? { spendEventId } : {},
|
|
11387
|
+
sandboxId: context.sandboxId || null,
|
|
11388
|
+
environmentId: context.environmentId || null,
|
|
11389
|
+
sessionId: context.sessionId || null,
|
|
11390
|
+
subjectId: context.subjectId || null,
|
|
11391
|
+
permissionProfileId: context.permissionProfileId || null,
|
|
11392
|
+
source: "openai",
|
|
11393
|
+
lineItemType: "llm_tokens",
|
|
11394
|
+
provider: options.usage.provider,
|
|
11395
|
+
model: options.usage.model,
|
|
11396
|
+
operation: options.usage.operation || "chat.completions",
|
|
11397
|
+
requestId: options.usage.requestId || null,
|
|
11398
|
+
inputTokens: options.usage.inputTokens,
|
|
11399
|
+
outputTokens: options.usage.outputTokens,
|
|
11400
|
+
cachedInputTokens: options.usage.cachedInputTokens,
|
|
11401
|
+
reasoningTokens: options.usage.reasoningTokens,
|
|
11402
|
+
quantity: options.usage.totalTokens,
|
|
11403
|
+
quantityUnit: "tokens",
|
|
11404
|
+
inputPricePerMillionMicros: options.usage.inputPricePerMillionMicros,
|
|
11405
|
+
cachedInputPricePerMillionMicros: options.usage.cachedInputPricePerMillionMicros,
|
|
11406
|
+
outputPricePerMillionMicros: options.usage.outputPricePerMillionMicros,
|
|
11407
|
+
amountMicros: options.usage.amountMicros,
|
|
11408
|
+
currency: options.usage.currency,
|
|
11409
|
+
pricingSource: options.usage.pricingSource,
|
|
11410
|
+
pricingEffectiveAt: pricingEffectiveAtSeconds(
|
|
11411
|
+
options.usage.pricingEffectiveAt
|
|
11412
|
+
),
|
|
11413
|
+
estimated: false,
|
|
11414
|
+
metadata
|
|
11415
|
+
})
|
|
11416
|
+
}
|
|
11417
|
+
);
|
|
11418
|
+
if (!response.ok) {
|
|
11419
|
+
throw new Error(
|
|
11420
|
+
`Granular spend event failed (${response.status}): ${await response.text()}`
|
|
11421
|
+
);
|
|
11422
|
+
}
|
|
11423
|
+
return response.json();
|
|
11424
|
+
}
|
|
11425
|
+
|
|
11309
11426
|
// ../metamodel-enum/src/index.ts
|
|
11310
11427
|
function renderInlineStringUnion(values) {
|
|
11311
11428
|
return values.map((value) => JSON.stringify(value)).join(" | ");
|
|
@@ -14088,7 +14205,7 @@ var EnvironmentSession = class extends Session {
|
|
|
14088
14205
|
/** The last known graph container status, updated by checkReadiness() or on heartbeat */
|
|
14089
14206
|
graphContainerStatus = null;
|
|
14090
14207
|
constructor(client, environment, clientId, options = {}) {
|
|
14091
|
-
super(client, clientId);
|
|
14208
|
+
super(client, clientId, { initialQuota: options.initialQuota });
|
|
14092
14209
|
this.environment = environment;
|
|
14093
14210
|
this.sessionDataRoutePrefix = options.sessionDataRoutePrefix || "/orchestrator/ws/sessions";
|
|
14094
14211
|
}
|
|
@@ -14887,6 +15004,15 @@ var Granular = class _Granular {
|
|
|
14887
15004
|
const environment = this.bindEnvironmentHandle(envData);
|
|
14888
15005
|
return this.bindWebSocketEnvironmentSession(environment, clientId, minted);
|
|
14889
15006
|
}
|
|
15007
|
+
async recordOpenAIUsageSpend(usage, context, options) {
|
|
15008
|
+
return recordOpenAIUsageSpend({
|
|
15009
|
+
apiUrl: this.apiUrl,
|
|
15010
|
+
token: this.apiKey,
|
|
15011
|
+
usage,
|
|
15012
|
+
context,
|
|
15013
|
+
metadata: options?.metadata
|
|
15014
|
+
});
|
|
15015
|
+
}
|
|
14890
15016
|
/**
|
|
14891
15017
|
* Mark a session closed in the control plane. If `environment` is the connected handle for that
|
|
14892
15018
|
* `sessionId`, disconnects the WebSocket so the runtime tears down cleanly.
|
|
@@ -15041,7 +15167,8 @@ var Granular = class _Granular {
|
|
|
15041
15167
|
const environmentSession = new EnvironmentSession(
|
|
15042
15168
|
client,
|
|
15043
15169
|
environment,
|
|
15044
|
-
clientId
|
|
15170
|
+
clientId,
|
|
15171
|
+
{ initialQuota: session.quota || null }
|
|
15045
15172
|
);
|
|
15046
15173
|
await environmentSession.hello();
|
|
15047
15174
|
return environmentSession;
|
|
@@ -17250,12 +17377,107 @@ ${knownFactsBlock}
|
|
|
17250
17377
|
${input.request?.trim() || "Use the latest user message in the conversation."}`;
|
|
17251
17378
|
}
|
|
17252
17379
|
|
|
17380
|
+
// src/openai-usage.ts
|
|
17381
|
+
var OPENAI_PRICING_SOURCE_URL = "https://developers.openai.com/api/docs/models/gpt-5.4/";
|
|
17382
|
+
var OPENAI_PRICING_EFFECTIVE_DATE = "2026-05-19";
|
|
17383
|
+
var OPENAI_MODEL_PRICING_USD_PER_MILLION = {
|
|
17384
|
+
"gpt-5.4": {
|
|
17385
|
+
provider: "openai",
|
|
17386
|
+
model: "gpt-5.4",
|
|
17387
|
+
currency: "USD",
|
|
17388
|
+
inputUsdPerMillion: 2.5,
|
|
17389
|
+
cachedInputUsdPerMillion: 0.25,
|
|
17390
|
+
outputUsdPerMillion: 15,
|
|
17391
|
+
sourceUrl: OPENAI_PRICING_SOURCE_URL,
|
|
17392
|
+
effectiveDate: OPENAI_PRICING_EFFECTIVE_DATE
|
|
17393
|
+
}
|
|
17394
|
+
};
|
|
17395
|
+
function asRecord5(value) {
|
|
17396
|
+
return value && typeof value === "object" ? value : null;
|
|
17397
|
+
}
|
|
17398
|
+
function numberField(record, key) {
|
|
17399
|
+
const value = record?.[key];
|
|
17400
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
17401
|
+
}
|
|
17402
|
+
function microsPerMillion(usdPerMillion) {
|
|
17403
|
+
return Math.round(usdPerMillion * 1e6);
|
|
17404
|
+
}
|
|
17405
|
+
function getOpenAIModelPricing(model) {
|
|
17406
|
+
return OPENAI_MODEL_PRICING_USD_PER_MILLION[model] || null;
|
|
17407
|
+
}
|
|
17408
|
+
function normalizeOpenAIUsage(rawUsage) {
|
|
17409
|
+
const usage = asRecord5(rawUsage);
|
|
17410
|
+
if (!usage) {
|
|
17411
|
+
return {
|
|
17412
|
+
inputTokens: 0,
|
|
17413
|
+
cachedInputTokens: 0,
|
|
17414
|
+
uncachedInputTokens: 0,
|
|
17415
|
+
outputTokens: 0,
|
|
17416
|
+
reasoningTokens: 0,
|
|
17417
|
+
totalTokens: 0
|
|
17418
|
+
};
|
|
17419
|
+
}
|
|
17420
|
+
const inputTokens = numberField(usage, "prompt_tokens") || numberField(usage, "input_tokens");
|
|
17421
|
+
const outputTokens = numberField(usage, "completion_tokens") || numberField(usage, "output_tokens");
|
|
17422
|
+
const totalTokens = numberField(usage, "total_tokens") || inputTokens + outputTokens;
|
|
17423
|
+
const inputDetails = asRecord5(usage.prompt_tokens_details) || asRecord5(usage.input_tokens_details);
|
|
17424
|
+
const outputDetails = asRecord5(usage.completion_tokens_details) || asRecord5(usage.output_tokens_details);
|
|
17425
|
+
const cachedInputTokens = Math.min(
|
|
17426
|
+
inputTokens,
|
|
17427
|
+
numberField(inputDetails, "cached_tokens") || numberField(inputDetails, "cached_input_tokens")
|
|
17428
|
+
);
|
|
17429
|
+
const reasoningTokens = numberField(outputDetails, "reasoning_tokens") || numberField(outputDetails, "reasoning_output_tokens");
|
|
17430
|
+
return {
|
|
17431
|
+
inputTokens,
|
|
17432
|
+
cachedInputTokens,
|
|
17433
|
+
uncachedInputTokens: Math.max(inputTokens - cachedInputTokens, 0),
|
|
17434
|
+
outputTokens,
|
|
17435
|
+
reasoningTokens,
|
|
17436
|
+
totalTokens
|
|
17437
|
+
};
|
|
17438
|
+
}
|
|
17439
|
+
function calculateOpenAITokenSpend(model, rawUsage) {
|
|
17440
|
+
const pricing = getOpenAIModelPricing(model);
|
|
17441
|
+
if (!pricing) return null;
|
|
17442
|
+
const usage = normalizeOpenAIUsage(rawUsage);
|
|
17443
|
+
const inputPricePerMillionMicros = microsPerMillion(
|
|
17444
|
+
pricing.inputUsdPerMillion
|
|
17445
|
+
);
|
|
17446
|
+
const cachedInputPricePerMillionMicros = microsPerMillion(
|
|
17447
|
+
pricing.cachedInputUsdPerMillion
|
|
17448
|
+
);
|
|
17449
|
+
const outputPricePerMillionMicros = microsPerMillion(
|
|
17450
|
+
pricing.outputUsdPerMillion
|
|
17451
|
+
);
|
|
17452
|
+
const amountMicros = Math.round(
|
|
17453
|
+
(usage.uncachedInputTokens * inputPricePerMillionMicros + usage.cachedInputTokens * cachedInputPricePerMillionMicros + usage.outputTokens * outputPricePerMillionMicros) / 1e6
|
|
17454
|
+
);
|
|
17455
|
+
return {
|
|
17456
|
+
provider: "openai",
|
|
17457
|
+
model,
|
|
17458
|
+
inputTokens: usage.inputTokens,
|
|
17459
|
+
cachedInputTokens: usage.cachedInputTokens,
|
|
17460
|
+
uncachedInputTokens: usage.uncachedInputTokens,
|
|
17461
|
+
outputTokens: usage.outputTokens,
|
|
17462
|
+
reasoningTokens: usage.reasoningTokens,
|
|
17463
|
+
totalTokens: usage.totalTokens,
|
|
17464
|
+
amountMicros,
|
|
17465
|
+
currency: "USD",
|
|
17466
|
+
inputPricePerMillionMicros,
|
|
17467
|
+
cachedInputPricePerMillionMicros,
|
|
17468
|
+
outputPricePerMillionMicros,
|
|
17469
|
+
pricingSource: pricing.sourceUrl,
|
|
17470
|
+
pricingEffectiveAt: pricing.effectiveDate,
|
|
17471
|
+
usage
|
|
17472
|
+
};
|
|
17473
|
+
}
|
|
17474
|
+
|
|
17253
17475
|
// src/agent-evals.ts
|
|
17254
17476
|
var DEFAULT_CONTROLLER_BUDGETS = {
|
|
17255
17477
|
maxIterations: 6,
|
|
17256
17478
|
maxNoProgressIterations: 2
|
|
17257
17479
|
};
|
|
17258
|
-
function
|
|
17480
|
+
function asRecord6(value) {
|
|
17259
17481
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
17260
17482
|
return value;
|
|
17261
17483
|
}
|
|
@@ -17308,9 +17530,9 @@ function asArray3(value) {
|
|
|
17308
17530
|
return Array.isArray(value) ? value : [value];
|
|
17309
17531
|
}
|
|
17310
17532
|
var GPT_54_TOKEN_PRICING_USD_PER_MILLION = {
|
|
17311
|
-
input:
|
|
17312
|
-
cachedInput: 0.
|
|
17313
|
-
output:
|
|
17533
|
+
input: 2.5,
|
|
17534
|
+
cachedInput: 0.25,
|
|
17535
|
+
output: 15
|
|
17314
17536
|
};
|
|
17315
17537
|
function emptyTokenUsage() {
|
|
17316
17538
|
return {
|
|
@@ -17327,14 +17549,24 @@ function emptyTokenUsage() {
|
|
|
17327
17549
|
missingUsageCalls: 0
|
|
17328
17550
|
};
|
|
17329
17551
|
}
|
|
17330
|
-
function
|
|
17552
|
+
function numberField2(record, key) {
|
|
17331
17553
|
const value = record?.[key];
|
|
17332
17554
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
17333
17555
|
}
|
|
17334
17556
|
function calculateTokenCost(input) {
|
|
17335
|
-
const
|
|
17336
|
-
|
|
17337
|
-
|
|
17557
|
+
const spend = input.model ? calculateOpenAITokenSpend(input.model, {
|
|
17558
|
+
input_tokens: input.uncachedInputTokens + input.cachedInputTokens,
|
|
17559
|
+
output_tokens: input.outputTokens,
|
|
17560
|
+
input_tokens_details: { cached_tokens: input.cachedInputTokens }
|
|
17561
|
+
}) : null;
|
|
17562
|
+
const pricing = spend ? {
|
|
17563
|
+
input: spend.inputPricePerMillionMicros / 1e6,
|
|
17564
|
+
cachedInput: spend.cachedInputPricePerMillionMicros / 1e6,
|
|
17565
|
+
output: spend.outputPricePerMillionMicros / 1e6
|
|
17566
|
+
} : GPT_54_TOKEN_PRICING_USD_PER_MILLION;
|
|
17567
|
+
const inputCostUsd = input.uncachedInputTokens * pricing.input / 1e6;
|
|
17568
|
+
const cachedInputCostUsd = input.cachedInputTokens * pricing.cachedInput / 1e6;
|
|
17569
|
+
const outputCostUsd = input.outputTokens * pricing.output / 1e6;
|
|
17338
17570
|
return {
|
|
17339
17571
|
inputCostUsd,
|
|
17340
17572
|
cachedInputCostUsd,
|
|
@@ -17343,18 +17575,20 @@ function calculateTokenCost(input) {
|
|
|
17343
17575
|
};
|
|
17344
17576
|
}
|
|
17345
17577
|
function extractTokenUsageFromRaw(raw) {
|
|
17346
|
-
const usage =
|
|
17578
|
+
const usage = asRecord6(asRecord6(raw)?.usage);
|
|
17347
17579
|
if (!usage) return null;
|
|
17348
|
-
const
|
|
17349
|
-
const
|
|
17350
|
-
const
|
|
17580
|
+
const model = typeof asRecord6(raw)?.model === "string" ? asRecord6(raw)?.model : void 0;
|
|
17581
|
+
const inputTokens = numberField2(usage, "prompt_tokens") || numberField2(usage, "input_tokens");
|
|
17582
|
+
const outputTokens = numberField2(usage, "completion_tokens") || numberField2(usage, "output_tokens");
|
|
17583
|
+
const details = asRecord6(usage.prompt_tokens_details) || asRecord6(usage.input_tokens_details);
|
|
17351
17584
|
const cachedInputTokens = Math.min(
|
|
17352
17585
|
inputTokens,
|
|
17353
|
-
|
|
17586
|
+
numberField2(details, "cached_tokens") || numberField2(details, "cached_input_tokens")
|
|
17354
17587
|
);
|
|
17355
17588
|
const uncachedInputTokens = Math.max(inputTokens - cachedInputTokens, 0);
|
|
17356
|
-
const totalTokens =
|
|
17589
|
+
const totalTokens = numberField2(usage, "total_tokens") || inputTokens + outputTokens;
|
|
17357
17590
|
const costs = calculateTokenCost({
|
|
17591
|
+
model,
|
|
17358
17592
|
uncachedInputTokens,
|
|
17359
17593
|
cachedInputTokens,
|
|
17360
17594
|
outputTokens
|
|
@@ -17405,7 +17639,7 @@ function aggregateTokenUsage(usages) {
|
|
|
17405
17639
|
}
|
|
17406
17640
|
function aggregateConversationTokenUsage(conversation) {
|
|
17407
17641
|
return aggregateTokenUsage(
|
|
17408
|
-
conversation.logTurns.flatMap(
|
|
17642
|
+
(conversation.logTurns || []).flatMap(
|
|
17409
17643
|
(turn) => turn.iterations.map((iteration) => iteration.tokenUsage)
|
|
17410
17644
|
)
|
|
17411
17645
|
);
|
|
@@ -17439,8 +17673,8 @@ function formatTokenUsage(usage) {
|
|
|
17439
17673
|
];
|
|
17440
17674
|
}
|
|
17441
17675
|
function getJobAgentMessages(liveDoc, jobId) {
|
|
17442
|
-
const jobsById =
|
|
17443
|
-
const job =
|
|
17676
|
+
const jobsById = asRecord6(asRecord6(liveDoc.jobs)?.byId);
|
|
17677
|
+
const job = asRecord6(jobsById?.[jobId]);
|
|
17444
17678
|
const agentMessages = job?.agentMessages;
|
|
17445
17679
|
return Array.isArray(agentMessages) ? agentMessages : [];
|
|
17446
17680
|
}
|
|
@@ -17499,12 +17733,12 @@ function buildHistory(entries) {
|
|
|
17499
17733
|
);
|
|
17500
17734
|
}
|
|
17501
17735
|
function getOpenPromptsFromDoc(liveDoc) {
|
|
17502
|
-
const jobsById =
|
|
17736
|
+
const jobsById = asRecord6(asRecord6(liveDoc?.jobs)?.byId) || {};
|
|
17503
17737
|
const prompts = [];
|
|
17504
17738
|
for (const job of Object.values(jobsById)) {
|
|
17505
|
-
const promptRecords =
|
|
17739
|
+
const promptRecords = asRecord6(asRecord6(job)?.prompts) || {};
|
|
17506
17740
|
for (const raw of Object.values(promptRecords)) {
|
|
17507
|
-
const record =
|
|
17741
|
+
const record = asRecord6(raw);
|
|
17508
17742
|
if (!record || record.status !== "open" || typeof record.promptId !== "string")
|
|
17509
17743
|
continue;
|
|
17510
17744
|
const prompt = normalizePrompt({
|
|
@@ -17525,11 +17759,11 @@ function getOpenPromptsFromDoc(liveDoc) {
|
|
|
17525
17759
|
return prompts;
|
|
17526
17760
|
}
|
|
17527
17761
|
function filterPromptsByBoundary(liveDoc, prompts, boundaryTimestamp) {
|
|
17528
|
-
const jobsById =
|
|
17762
|
+
const jobsById = asRecord6(asRecord6(liveDoc?.jobs)?.byId) || {};
|
|
17529
17763
|
return prompts.filter((prompt) => {
|
|
17530
17764
|
for (const jobRecord of Object.values(jobsById)) {
|
|
17531
|
-
const promptsById =
|
|
17532
|
-
const promptRecord =
|
|
17765
|
+
const promptsById = asRecord6(asRecord6(jobRecord)?.prompts) || {};
|
|
17766
|
+
const promptRecord = asRecord6(promptsById[prompt.id]);
|
|
17533
17767
|
const openedAt = Number(promptRecord?.openedAt) || 0;
|
|
17534
17768
|
if (openedAt >= boundaryTimestamp) return true;
|
|
17535
17769
|
}
|
|
@@ -17661,6 +17895,29 @@ function createOpenAIChatTurnGenerator(options) {
|
|
|
17661
17895
|
""
|
|
17662
17896
|
);
|
|
17663
17897
|
const model = options.model || "gpt-5.4";
|
|
17898
|
+
const client = new OpenAI({
|
|
17899
|
+
apiKey: options.apiKey,
|
|
17900
|
+
baseURL: baseUrl,
|
|
17901
|
+
defaultHeaders: options.headers
|
|
17902
|
+
});
|
|
17903
|
+
const emitUsage = async (rawUsage, requestId, usageContext) => {
|
|
17904
|
+
if (!rawUsage || !options.onUsage) return;
|
|
17905
|
+
const spend = calculateOpenAITokenSpend(model, rawUsage);
|
|
17906
|
+
if (!spend) return;
|
|
17907
|
+
const mergedUsageContext = {
|
|
17908
|
+
...options.usageContext || {},
|
|
17909
|
+
...usageContext || {}
|
|
17910
|
+
};
|
|
17911
|
+
await options.onUsage({
|
|
17912
|
+
...spend,
|
|
17913
|
+
source: "openai",
|
|
17914
|
+
lineItemType: "llm_tokens",
|
|
17915
|
+
operation: "chat.completions",
|
|
17916
|
+
requestId: requestId || null,
|
|
17917
|
+
usageContext: mergedUsageContext,
|
|
17918
|
+
rawUsage
|
|
17919
|
+
});
|
|
17920
|
+
};
|
|
17664
17921
|
return async (input) => {
|
|
17665
17922
|
const messages = [
|
|
17666
17923
|
{
|
|
@@ -17677,80 +17934,46 @@ ${modelOutputInstruction()}`
|
|
|
17677
17934
|
messages,
|
|
17678
17935
|
response_format: { type: "json_object" }
|
|
17679
17936
|
};
|
|
17680
|
-
if (input.onTextDelta) {
|
|
17681
|
-
payload.stream = true;
|
|
17682
|
-
}
|
|
17683
17937
|
if (typeof options.temperature === "number") {
|
|
17684
17938
|
payload.temperature = options.temperature;
|
|
17685
17939
|
}
|
|
17686
17940
|
let lastError = null;
|
|
17687
17941
|
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
17688
17942
|
try {
|
|
17689
|
-
const response = await fetch(`${baseUrl}/chat/completions`, {
|
|
17690
|
-
method: "POST",
|
|
17691
|
-
headers: {
|
|
17692
|
-
"content-type": "application/json",
|
|
17693
|
-
authorization: `Bearer ${options.apiKey}`,
|
|
17694
|
-
...options.headers
|
|
17695
|
-
},
|
|
17696
|
-
body: JSON.stringify(payload)
|
|
17697
|
-
});
|
|
17698
|
-
if (!response.ok) {
|
|
17699
|
-
const errorText = await response.text();
|
|
17700
|
-
if (attempt < 3 && (response.status >= 500 || response.status === 429)) {
|
|
17701
|
-
await sleep2(500 * attempt);
|
|
17702
|
-
continue;
|
|
17703
|
-
}
|
|
17704
|
-
throw new Error(
|
|
17705
|
-
`OpenAI chat generation failed: ${response.status} ${errorText}`
|
|
17706
|
-
);
|
|
17707
|
-
}
|
|
17708
17943
|
let raw;
|
|
17709
17944
|
let text = "";
|
|
17710
|
-
|
|
17945
|
+
let usage = null;
|
|
17946
|
+
let requestId = null;
|
|
17947
|
+
if (input.onTextDelta) {
|
|
17711
17948
|
const onTextDelta = input.onTextDelta;
|
|
17712
|
-
const
|
|
17713
|
-
|
|
17714
|
-
|
|
17715
|
-
|
|
17716
|
-
|
|
17717
|
-
|
|
17718
|
-
|
|
17719
|
-
|
|
17720
|
-
const
|
|
17721
|
-
const
|
|
17722
|
-
|
|
17723
|
-
)?.content;
|
|
17724
|
-
const deltaText = typeof delta === "string" ? delta : Array.isArray(delta) ? delta.map((part) => asRecord5(part)?.text || "").join("") : "";
|
|
17725
|
-
if (!deltaText) return;
|
|
17949
|
+
const stream = await client.chat.completions.create({
|
|
17950
|
+
...payload,
|
|
17951
|
+
stream: true,
|
|
17952
|
+
stream_options: { include_usage: true }
|
|
17953
|
+
});
|
|
17954
|
+
for await (const event of stream) {
|
|
17955
|
+
requestId = requestId || event.id || event._request_id || null;
|
|
17956
|
+
usage = event.usage || usage;
|
|
17957
|
+
const delta = event.choices?.[0]?.delta?.content;
|
|
17958
|
+
const deltaText = typeof delta === "string" ? delta : Array.isArray(delta) ? delta.map((part) => asRecord6(part)?.text || "").join("") : "";
|
|
17959
|
+
if (!deltaText) continue;
|
|
17726
17960
|
text += deltaText;
|
|
17727
17961
|
await onTextDelta(deltaText);
|
|
17728
|
-
};
|
|
17729
|
-
while (true) {
|
|
17730
|
-
const { value, done } = await reader.read();
|
|
17731
|
-
if (done) break;
|
|
17732
|
-
buffer += decoder.decode(value, { stream: true });
|
|
17733
|
-
while (true) {
|
|
17734
|
-
const lineEnd = buffer.indexOf("\n");
|
|
17735
|
-
if (lineEnd === -1) break;
|
|
17736
|
-
const line = buffer.slice(0, lineEnd);
|
|
17737
|
-
buffer = buffer.slice(lineEnd + 1);
|
|
17738
|
-
await processStreamLine(line);
|
|
17739
|
-
}
|
|
17740
17962
|
}
|
|
17741
|
-
|
|
17742
|
-
if (buffer.trim()) {
|
|
17743
|
-
await processStreamLine(buffer);
|
|
17744
|
-
}
|
|
17745
|
-
raw = { streamed: true };
|
|
17963
|
+
raw = { streamed: true, model, usage, request_id: requestId };
|
|
17746
17964
|
} else {
|
|
17747
|
-
const
|
|
17748
|
-
|
|
17749
|
-
|
|
17750
|
-
|
|
17965
|
+
const completion = await client.chat.completions.create(
|
|
17966
|
+
payload
|
|
17967
|
+
);
|
|
17968
|
+
raw = completion;
|
|
17969
|
+
usage = completion.usage;
|
|
17970
|
+
requestId = (typeof completion.id === "string" ? completion.id : null) || (typeof completion._request_id === "string" ? completion._request_id : null);
|
|
17971
|
+
const content = asRecord6(
|
|
17972
|
+
asRecord6(completion.choices?.[0])?.message
|
|
17751
17973
|
)?.content;
|
|
17752
|
-
text = typeof content === "string" ? content : Array.isArray(content) ? content.map((part) =>
|
|
17974
|
+
text = typeof content === "string" ? content : Array.isArray(content) ? content.map((part) => asRecord6(part)?.text || "").join("") : "";
|
|
17753
17975
|
}
|
|
17976
|
+
await emitUsage(usage, requestId, input.usageContext);
|
|
17754
17977
|
const parsed = extractJsonObject(text);
|
|
17755
17978
|
if (!parsed) {
|
|
17756
17979
|
if (attempt < 3) {
|
|
@@ -17770,9 +17993,10 @@ ${modelOutputInstruction()}`
|
|
|
17770
17993
|
};
|
|
17771
17994
|
} catch (error) {
|
|
17772
17995
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
17773
|
-
|
|
17996
|
+
const status = Number(error?.status);
|
|
17997
|
+
if (attempt < 3 && (Number.isFinite(status) && (status >= 500 || status === 429) || /socket connection was closed unexpectedly|ECONNRESET|network|timed out/i.test(
|
|
17774
17998
|
lastError.message
|
|
17775
|
-
)) {
|
|
17999
|
+
))) {
|
|
17776
18000
|
await sleep2(500 * attempt);
|
|
17777
18001
|
continue;
|
|
17778
18002
|
}
|
|
@@ -17805,12 +18029,12 @@ async function withTimeout2(promise, ms, label) {
|
|
|
17805
18029
|
}
|
|
17806
18030
|
}
|
|
17807
18031
|
function getActionSummary(liveDoc, jobId) {
|
|
17808
|
-
const jobsById =
|
|
17809
|
-
const job =
|
|
18032
|
+
const jobsById = asRecord6(asRecord6(liveDoc?.jobs)?.byId) || {};
|
|
18033
|
+
const job = asRecord6(jobsById[jobId]);
|
|
17810
18034
|
const summary = Array.isArray(job?.actionSummary) ? job.actionSummary.filter(
|
|
17811
18035
|
(line) => typeof line === "string"
|
|
17812
18036
|
) : [];
|
|
17813
|
-
const trace = Array.isArray(job?.actionTrace) ? job.actionTrace.map((event) =>
|
|
18037
|
+
const trace = Array.isArray(job?.actionTrace) ? job.actionTrace.map((event) => asRecord6(event)).filter((event) => Boolean(event)) : [];
|
|
17814
18038
|
const traceLines = trace.map((event) => {
|
|
17815
18039
|
const kind = typeof event.kind === "string" ? event.kind : "";
|
|
17816
18040
|
const action = typeof event.action === "string" ? event.action : "";
|
|
@@ -17831,9 +18055,9 @@ function getActionSummary(liveDoc, jobId) {
|
|
|
17831
18055
|
}
|
|
17832
18056
|
function normalizeHeapSnapshot2(heap) {
|
|
17833
18057
|
return {
|
|
17834
|
-
entriesByPath:
|
|
17835
|
-
listsByName:
|
|
17836
|
-
variablesByName:
|
|
18058
|
+
entriesByPath: asRecord6(heap?.entriesByPath) || {},
|
|
18059
|
+
listsByName: asRecord6(heap?.listsByName) || {},
|
|
18060
|
+
variablesByName: asRecord6(heap?.variablesByName) || {},
|
|
17837
18061
|
updatedAt: typeof heap?.updatedAt === "number" ? heap.updatedAt : Date.now()
|
|
17838
18062
|
};
|
|
17839
18063
|
}
|
|
@@ -17868,9 +18092,9 @@ async function waitForJobOutcome(input) {
|
|
|
17868
18092
|
input.boundaryTimestamp
|
|
17869
18093
|
);
|
|
17870
18094
|
lastPromptCount = prompts.length;
|
|
17871
|
-
const messages = asArray3(
|
|
18095
|
+
const messages = asArray3(asRecord6(liveDoc.conversation)?.messages);
|
|
17872
18096
|
lastMessageCount = messages.length;
|
|
17873
|
-
lastJobSummary =
|
|
18097
|
+
lastJobSummary = asRecord6(asRecord6(liveDoc.jobs)?.byId)?.[input.job.id] || null;
|
|
17874
18098
|
if (prompts.length > 0) {
|
|
17875
18099
|
return { kind: "prompt", prompts, liveDoc, stdout, stderr };
|
|
17876
18100
|
}
|
|
@@ -18054,7 +18278,8 @@ function jsonBlock(value) {
|
|
|
18054
18278
|
}
|
|
18055
18279
|
function buildSessionLogReport(input) {
|
|
18056
18280
|
const { conversation, result, error } = input;
|
|
18057
|
-
const
|
|
18281
|
+
const logTurns = conversation.logTurns || [];
|
|
18282
|
+
const systemPrompts = logTurns.flatMap(
|
|
18058
18283
|
(turn) => turn.iterations.map((iteration) => ({
|
|
18059
18284
|
turn,
|
|
18060
18285
|
iteration
|
|
@@ -18083,7 +18308,7 @@ function buildSessionLogReport(input) {
|
|
|
18083
18308
|
"",
|
|
18084
18309
|
"## Conversation"
|
|
18085
18310
|
];
|
|
18086
|
-
for (const turn of
|
|
18311
|
+
for (const turn of logTurns) {
|
|
18087
18312
|
lines.push("", `### Turn ${turn.turnNumber}: ${turn.turnId}`, "");
|
|
18088
18313
|
lines.push("**User**", "");
|
|
18089
18314
|
lines.push(turn.request, "");
|
|
@@ -18281,7 +18506,7 @@ async function runAgentEvalSuite(options) {
|
|
|
18281
18506
|
promptInteractions: completed.promptInteractions,
|
|
18282
18507
|
result: completed.result,
|
|
18283
18508
|
heap: normalizeHeapSnapshot2(
|
|
18284
|
-
|
|
18509
|
+
asRecord6(
|
|
18285
18510
|
cloneJson(conversation.environment.document)?.heap
|
|
18286
18511
|
)
|
|
18287
18512
|
),
|
|
@@ -18391,7 +18616,7 @@ async function runAgentEvalSuite(options) {
|
|
|
18391
18616
|
finalResult = result;
|
|
18392
18617
|
} catch (error) {
|
|
18393
18618
|
const failureMessage = error instanceof Error ? error.message : String(error);
|
|
18394
|
-
const failedTurn = conversation.logTurns[conversation.logTurns.length - 1];
|
|
18619
|
+
const failedTurn = conversation.logTurns?.[conversation.logTurns.length - 1];
|
|
18395
18620
|
if (failedTurn && !failedTurn.completed) {
|
|
18396
18621
|
failedTurn.error = failureMessage;
|
|
18397
18622
|
const failedIteration = latestIterationLog(failedTurn);
|
|
@@ -18524,7 +18749,7 @@ function createAgentEvalHarness(options) {
|
|
|
18524
18749
|
}
|
|
18525
18750
|
function buildCheckContext(conversation, completed, turnDir) {
|
|
18526
18751
|
const liveDoc = cloneJson(conversation.environment.document);
|
|
18527
|
-
const heap = normalizeHeapSnapshot2(
|
|
18752
|
+
const heap = normalizeHeapSnapshot2(asRecord6(liveDoc?.heap));
|
|
18528
18753
|
return {
|
|
18529
18754
|
conversation,
|
|
18530
18755
|
environment: conversation.environment,
|
|
@@ -18618,7 +18843,7 @@ function createAgentEvalHarness(options) {
|
|
|
18618
18843
|
result: resumed.result,
|
|
18619
18844
|
stdout: [...pending.stdout, ...resumed.stdout],
|
|
18620
18845
|
agentMessages: getJobAgentMessages(liveDoc, pending.job.id),
|
|
18621
|
-
sessionHeap: normalizeHeapSnapshot2(
|
|
18846
|
+
sessionHeap: normalizeHeapSnapshot2(asRecord6(liveDoc?.heap))
|
|
18622
18847
|
});
|
|
18623
18848
|
const responseText = presentation.responseText || pending.finalReply || "Done.";
|
|
18624
18849
|
pending.conversation.history.push({
|
|
@@ -18742,7 +18967,7 @@ function createAgentEvalHarness(options) {
|
|
|
18742
18967
|
environmentId: conversation.environment.environmentId,
|
|
18743
18968
|
domainRevision: conversation.environment.domainRevision
|
|
18744
18969
|
},
|
|
18745
|
-
heapSummary: projectHeapSummary(
|
|
18970
|
+
heapSummary: projectHeapSummary(asRecord6(liveDoc?.heap), {
|
|
18746
18971
|
focus: heapFocus
|
|
18747
18972
|
}),
|
|
18748
18973
|
referentSummary: projectConversationReferentSummary(liveDoc),
|
|
@@ -18764,7 +18989,14 @@ function createAgentEvalHarness(options) {
|
|
|
18764
18989
|
history: buildHistory(conversation.history),
|
|
18765
18990
|
request,
|
|
18766
18991
|
attempt: 1,
|
|
18767
|
-
tools
|
|
18992
|
+
tools,
|
|
18993
|
+
usageContext: {
|
|
18994
|
+
sandboxId: conversation.environment.sandboxId,
|
|
18995
|
+
environmentId: conversation.environment.environmentId,
|
|
18996
|
+
sessionId: conversation.environment.sessionId,
|
|
18997
|
+
subjectId: conversation.environment.subjectId,
|
|
18998
|
+
permissionProfileId: conversation.environment.permissionProfileId
|
|
18999
|
+
}
|
|
18768
19000
|
}),
|
|
18769
19001
|
chatTimeoutMs,
|
|
18770
19002
|
`chat generation for ${conversation.label} iteration ${iteration + 1}`
|
|
@@ -18913,7 +19145,7 @@ function createAgentEvalHarness(options) {
|
|
|
18913
19145
|
const settledLiveDoc = cloneJson(
|
|
18914
19146
|
conversation.environment.document
|
|
18915
19147
|
);
|
|
18916
|
-
const sessionHeap = normalizeHeapSnapshot2(
|
|
19148
|
+
const sessionHeap = normalizeHeapSnapshot2(asRecord6(settledLiveDoc?.heap));
|
|
18917
19149
|
const presentation = resolveJobPresentation({
|
|
18918
19150
|
jobId: job.id,
|
|
18919
19151
|
result: outcome.result,
|
|
@@ -19037,7 +19269,10 @@ function createAgentTester(options) {
|
|
|
19037
19269
|
model: options.openai?.model || options.model,
|
|
19038
19270
|
baseUrl: options.openai?.baseUrl,
|
|
19039
19271
|
temperature: options.openai?.temperature,
|
|
19040
|
-
headers: options.openai?.headers
|
|
19272
|
+
headers: options.openai?.headers,
|
|
19273
|
+
onUsage: async (usage) => {
|
|
19274
|
+
await granular.recordOpenAIUsageSpend(usage, usage.usageContext);
|
|
19275
|
+
}
|
|
19041
19276
|
});
|
|
19042
19277
|
let resolvedEnvironmentId = "environmentId" in options.target ? options.target.environmentId : null;
|
|
19043
19278
|
let connectSeeded = false;
|