humanish 0.18.0 → 0.19.0
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 +21 -0
- package/dist/actor-contract.d.ts +10 -0
- package/dist/actor-contract.js.map +1 -1
- package/dist/computer-use-actor.d.ts +4 -0
- package/dist/computer-use-actor.js +3 -1
- package/dist/computer-use-actor.js.map +1 -1
- package/dist/computer-use.d.ts +16 -0
- package/dist/computer-use.js +26 -1
- package/dist/computer-use.js.map +1 -1
- package/dist/concurrent-shared-world-lab.js +1 -0
- package/dist/concurrent-shared-world-lab.js.map +1 -1
- package/dist/cua-actor-lab.d.ts +33 -3
- package/dist/cua-actor-lab.js +169 -2
- package/dist/cua-actor-lab.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lab-config.d.ts +12 -0
- package/dist/lab-config.js +12 -0
- package/dist/lab-config.js.map +1 -1
- package/dist/observer-assets.js +37 -3
- package/dist/observer-assets.js.map +1 -1
- package/dist/observer-data.d.ts +7 -1
- package/dist/observer-data.js +1 -0
- package/dist/observer-data.js.map +1 -1
- package/dist/observer-library.d.ts +4 -0
- package/dist/observer-library.js +8 -1
- package/dist/observer-library.js.map +1 -1
- package/dist/observer.d.ts +3 -0
- package/dist/observer.js +5 -1
- package/dist/observer.js.map +1 -1
- package/dist/pricing.d.ts +78 -0
- package/dist/pricing.js +100 -0
- package/dist/pricing.js.map +1 -0
- package/dist/run.d.ts +54 -0
- package/dist/run.js +86 -0
- package/dist/run.js.map +1 -1
- package/docs/architecture/actor-contract.md +12 -0
- package/docs/contracts/run-bundle.md +23 -0
- package/docs/contracts/schemas.md +89 -2
- package/docs/goals/current.md +1 -1
- package/docs/ramp/README.md +1 -1
- package/package.json +1 -1
package/dist/cua-actor-lab.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ActorCompletionReason, ActorPersonaRef, ActorStatus } from "./actor-contract.js";
|
|
1
|
+
import type { ActorCompletionReason, ActorPersonaRef, ActorStatus, ActorTrace } from "./actor-contract.js";
|
|
2
2
|
import { type BrowserLabAdapterHooks } from "./adapter-extension.js";
|
|
3
3
|
import { type CuaActorDescriptor } from "./actor-registry.js";
|
|
4
4
|
import type { CuaActorSessionOptions } from "./computer-use-actor.js";
|
|
@@ -11,7 +11,7 @@ import { renderObserver, type ObserverResult } from "./observer.js";
|
|
|
11
11
|
import { type PreparedOutputDirectory } from "./selected-output-paths.js";
|
|
12
12
|
import { type LocalTreeArchive } from "./source-archive.js";
|
|
13
13
|
import type { StopWhen } from "./stop-conditions.js";
|
|
14
|
-
import { type RunBundle, type RunDesktopGeometry, type RunRerunLineage, type RunProviderResource, type RunSubjectProvenance, type RunSubjectStateStepRecord } from "./run.js";
|
|
14
|
+
import { type RunBundle, type RunDesktopGeometry, type RunRerunLineage, type RunProviderResource, type RunCostSummary, type RunSubjectProvenance, type RunSubjectStateStepRecord } from "./run.js";
|
|
15
15
|
export declare const CUA_ACTOR_LAB_SCHEMA = "humanish.cua-lab-result.v2";
|
|
16
16
|
export declare const CUA_FANOUT_STRATEGY: "per-lane-worlds";
|
|
17
17
|
export declare const CUA_ACTOR_LAB_PROVIDER_METADATA: {
|
|
@@ -125,6 +125,10 @@ export interface CuaActorLabHooks extends BrowserLabAdapterHooks {
|
|
|
125
125
|
}) => Promise<CuaProvider>;
|
|
126
126
|
env?: Record<string, string | undefined>;
|
|
127
127
|
renderObserverFn?: typeof renderObserver;
|
|
128
|
+
/** Injected clock (ms) for the host-side E2B desktop create->teardown span measurement that
|
|
129
|
+
* feeds the desktop-minute cost estimate. Defaults to Date.now; tests inject a frozen/stepped
|
|
130
|
+
* clock so the desktop-minute line is deterministic. */
|
|
131
|
+
now?: () => number;
|
|
128
132
|
/** Injected clock/sleep for the detached-step polling (tests only). */
|
|
129
133
|
detachedTimers?: DetachedTimers;
|
|
130
134
|
/**
|
|
@@ -246,7 +250,7 @@ export interface CuaLaneSummary {
|
|
|
246
250
|
concurrency: number;
|
|
247
251
|
waves: number;
|
|
248
252
|
}
|
|
249
|
-
export type CuaActorLabErrorCode = "HUMANISH_CUA_LAB_FAILED" | "HUMANISH_CUA_LAB_KEYS_MISSING" | "HUMANISH_CUA_LAB_SUBJECT_ENV_MISSING" | "HUMANISH_CUA_LAB_ACTOR_UNSUPPORTED" | "HUMANISH_CUA_LAB_SUBJECT_INVALID" | "HUMANISH_CUA_LAB_SUBJECT_UNSAFE" | "HUMANISH_CUA_LAB_EXECUTOR_NO_PROVIDER" | "HUMANISH_CUA_LAB_LOCAL_APP_NO_EXECUTOR" | "HUMANISH_CUA_LAB_FANOUT_INVALID" | "HUMANISH_CUA_LAB_RERUN_INVALID" | "HUMANISH_CUA_LAB_DEVICE_GEOMETRY" | "HUMANISH_WATCH_ALLOW_REQUIRES_OAUTH" | "HUMANISH_WATCH_OAUTH_REQUIRES_TUNNEL" | "HUMANISH_WATCH_OPTION_CONFLICT" | "HUMANISH_WATCH_TUNNEL_REQUIRES_EXPOSE" | "HUMANISH_WATCH_EXPOSE_REQUIRES_EDGE_AUTH" | "HUMANISH_WATCH_EXPOSE_REQUIRES_LIVE_FOLLOW" | "HUMANISH_WATCH_SAFE_NOT_APPLICABLE" | "HUMANISH_SERVE_TUNNEL_NOT_FOUND" | "HUMANISH_SERVE_TUNNEL_START_FAILED";
|
|
253
|
+
export type CuaActorLabErrorCode = "HUMANISH_CUA_LAB_FAILED" | "HUMANISH_CUA_LAB_KEYS_MISSING" | "HUMANISH_CUA_LAB_SUBJECT_ENV_MISSING" | "HUMANISH_CUA_LAB_ACTOR_UNSUPPORTED" | "HUMANISH_CUA_LAB_SUBJECT_INVALID" | "HUMANISH_CUA_LAB_SUBJECT_UNSAFE" | "HUMANISH_CUA_LAB_EXECUTOR_NO_PROVIDER" | "HUMANISH_CUA_LAB_LOCAL_APP_NO_EXECUTOR" | "HUMANISH_CUA_LAB_FANOUT_INVALID" | "HUMANISH_CUA_LAB_RERUN_INVALID" | "HUMANISH_CUA_LAB_DEVICE_GEOMETRY" | "HUMANISH_CUA_LAB_UNPRICED_CAP" | "HUMANISH_WATCH_ALLOW_REQUIRES_OAUTH" | "HUMANISH_WATCH_OAUTH_REQUIRES_TUNNEL" | "HUMANISH_WATCH_OPTION_CONFLICT" | "HUMANISH_WATCH_TUNNEL_REQUIRES_EXPOSE" | "HUMANISH_WATCH_EXPOSE_REQUIRES_EDGE_AUTH" | "HUMANISH_WATCH_EXPOSE_REQUIRES_LIVE_FOLLOW" | "HUMANISH_WATCH_SAFE_NOT_APPLICABLE" | "HUMANISH_SERVE_TUNNEL_NOT_FOUND" | "HUMANISH_SERVE_TUNNEL_START_FAILED";
|
|
250
254
|
/** Subject provenance projection (invariant 5): what the actor actually drove. */
|
|
251
255
|
export interface CuaSubjectProjection {
|
|
252
256
|
source: "app-url" | "clone" | "local-tree";
|
|
@@ -416,6 +420,9 @@ export interface CuaLaneDeps {
|
|
|
416
420
|
redactScreenshots: boolean;
|
|
417
421
|
scrubKnownValues: (text: string) => string;
|
|
418
422
|
runSession: (options: CuaActorSessionOptions) => Promise<CuaLoopResult>;
|
|
423
|
+
/** Injected clock (ms). Used to measure the host-side E2B desktop create->teardown span so the
|
|
424
|
+
* desktop-minute cost estimate is deterministic in tests. Defaults to Date.now. */
|
|
425
|
+
now: () => number;
|
|
419
426
|
hooks: CuaActorLabHooks;
|
|
420
427
|
/** Lane-0 only: signal the pipeline gate after provisioning succeeds (true) or fails (false). */
|
|
421
428
|
signalProvisioned?: (ok: boolean) => void;
|
|
@@ -434,6 +441,10 @@ export interface LaneRunOutcome {
|
|
|
434
441
|
session?: CuaLoopResult;
|
|
435
442
|
sessionError?: string;
|
|
436
443
|
sandboxId?: string;
|
|
444
|
+
/** Host-side E2B desktop create->teardown span (ms). An APPROXIMATION of E2B's server-side
|
|
445
|
+
* billed lifetime (server-side kill-on-timeout can extend it) — so the derived dollar figure is
|
|
446
|
+
* doubly an estimate. Absent on the in-process route (no sandbox) and on dry-run. */
|
|
447
|
+
desktopDurationMs?: number;
|
|
437
448
|
killed: boolean;
|
|
438
449
|
streamUrlPresent: boolean;
|
|
439
450
|
screenshots: string[];
|
|
@@ -635,6 +646,22 @@ export declare function resolveSubjectState(args: {
|
|
|
635
646
|
* `stream.actor = session.trace` — the provider-neutral ActorTrace seam the Observer renders.
|
|
636
647
|
* Exported for the bundle-builder tests.
|
|
637
648
|
*/
|
|
649
|
+
/**
|
|
650
|
+
* Assemble the run-level cost ESTIMATE from each lane's persisted per-actor estimate
|
|
651
|
+
* (trace.estimatedCost, set at the lab boundary) plus the aggregated E2B desktop-minute span.
|
|
652
|
+
* Returns undefined (cost OMITTED) when nothing was priceable AND no sandbox ran — a pure dry-run
|
|
653
|
+
* or an in-process lane (no trace.estimatedCost, no desktop) stays byte-stable with no cost block.
|
|
654
|
+
* The null-discipline mirrors the terminal ledger: a present-but-unpriceable line is null + a
|
|
655
|
+
* reason and contributes NOTHING to estimatedTotalUsd (never coerced to 0); an all-null summary
|
|
656
|
+
* has a null total. Every non-null figure carries its ratesAsOf date + source (invariant 6).
|
|
657
|
+
*/
|
|
658
|
+
export declare function buildCuaCostSummary(args: {
|
|
659
|
+
lanes: Array<{
|
|
660
|
+
laneId?: string;
|
|
661
|
+
trace: ActorTrace;
|
|
662
|
+
}>;
|
|
663
|
+
desktopMinutes: number | undefined;
|
|
664
|
+
}): RunCostSummary | undefined;
|
|
638
665
|
export declare function buildCuaBundle(args: {
|
|
639
666
|
actorId: string;
|
|
640
667
|
appUrl: string;
|
|
@@ -687,6 +714,9 @@ export declare function buildCuaBundle(args: {
|
|
|
687
714
|
/** Completed subject-phase records (clone/upload/extract/install/build/ready/state groups)
|
|
688
715
|
* to fold into bundle.events, so run.json carries real phase timing after the fact. */
|
|
689
716
|
phaseEvents?: SubjectPhaseEvent[];
|
|
717
|
+
/** Host-side E2B desktop billed span for this lane, in minutes (from LaneRunOutcome
|
|
718
|
+
* desktopDurationMs). Absent when no sandbox ran (in-process/dry-run) → no desktop cost line. */
|
|
719
|
+
desktopMinutes?: number;
|
|
690
720
|
}): RunBundle;
|
|
691
721
|
/**
|
|
692
722
|
* Project N>1 fan-out lanes into a humanish.run-bundle.v1 (the evidence schema is unchanged; this
|
package/dist/cua-actor-lab.js
CHANGED
|
@@ -28,6 +28,7 @@ import { pathToFileURL } from "node:url";
|
|
|
28
28
|
import { adapterScoreFailureMessage, applyBrowserAdapterHooks } from "./adapter-extension.js";
|
|
29
29
|
import { actorRegistry, isCuaActorDescriptor } from "./actor-registry.js";
|
|
30
30
|
import { CHROMIUM_EVIDENCE_HYGIENE_FLAGS, chromiumEvidenceProfilePreferencesJson } from "./browser-evidence-hygiene.js";
|
|
31
|
+
import { DEFAULT_OPENAI_CU_MODEL } from "./openai-responses-cu.js";
|
|
31
32
|
import { createDesktopSandbox, loadE2BDesktopModule } from "./e2b-desktop-launch.js";
|
|
32
33
|
import { probeUrl, readDetachedLog, runDetachedStep, startDetachedProcess } from "./e2b-detached.js";
|
|
33
34
|
import { DEFAULT_DEVICE_PRESET, isDevicePresetName, resolveDevicePreset } from "./device-presets.js";
|
|
@@ -41,6 +42,7 @@ import { assertPreparedSelectedOutputDirectory, assertSafeOutputPathSegment, pre
|
|
|
41
42
|
import { prepareRunArtifactPaths, validatePreparedRunArtifactPaths } from "./run-paths.js";
|
|
42
43
|
import { createLocalTreeArchive } from "./source-archive.js";
|
|
43
44
|
import { buildRunSource, loadRunBundle, PUBLIC_TARGET_CWD, REVIEW_SCHEMA, RUN_BUNDLE_SCHEMA } from "./run.js";
|
|
45
|
+
import { estimateActorCost, estimateDesktopCost, MODEL_RATES, round6 } from "./pricing.js";
|
|
44
46
|
export const CUA_ACTOR_LAB_SCHEMA = "humanish.cua-lab-result.v2";
|
|
45
47
|
// The only fan-out topology this slice ships: N lanes = N independent E2B desktop sandboxes,
|
|
46
48
|
// each its own world (clone/serve + subject.state per lane). Shared-world is layer 7 (#164).
|
|
@@ -964,6 +966,11 @@ export async function runCuaLane(spec, deps) {
|
|
|
964
966
|
let sessionError;
|
|
965
967
|
let failureCode;
|
|
966
968
|
let sandboxId;
|
|
969
|
+
// Host-side E2B desktop billed-span endpoints, measured via the injected clock. Captured right
|
|
970
|
+
// after create() succeeds and again in the finally after teardown resolves (both the killed and
|
|
971
|
+
// kept-for-debug paths), so the desktop-minute cost estimate reflects the honest lifetime.
|
|
972
|
+
let sandboxCreatedAtMs;
|
|
973
|
+
let sandboxTornDownAtMs;
|
|
967
974
|
let killed = false;
|
|
968
975
|
let streamUrl;
|
|
969
976
|
let subjectCommit;
|
|
@@ -1013,6 +1020,8 @@ export async function runCuaLane(spec, deps) {
|
|
|
1013
1020
|
lifecycle: { onTimeout: "kill" }
|
|
1014
1021
|
}, config.execution?.desktop?.template);
|
|
1015
1022
|
sandboxId = desktop.sandboxId;
|
|
1023
|
+
// The billed span starts the instant the sandbox exists.
|
|
1024
|
+
sandboxCreatedAtMs = deps.now();
|
|
1016
1025
|
if (deps.hooks.prepareDesktop) {
|
|
1017
1026
|
await deps.hooks.prepareDesktop(desktop, { laneId: spec.laneId, laneIndex: spec.laneIndex, laneCount: deps.laneCount });
|
|
1018
1027
|
}
|
|
@@ -1127,6 +1136,12 @@ export async function runCuaLane(spec, deps) {
|
|
|
1127
1136
|
catch (error) {
|
|
1128
1137
|
warnings.push(`Live desktop stream unavailable (run continues; evidence still captured): ${redactText(deps.scrubKnownValues(toErrorMessage(error)))}`);
|
|
1129
1138
|
}
|
|
1139
|
+
// The FAIL-CLOSED spend cap (execution.caps.maxUsd) is wired into the loop as maxUsd + an
|
|
1140
|
+
// injected pure per-turn estimator keyed on the resolved model. Preflight already refused a
|
|
1141
|
+
// cap on an unpriced model, so the estimate is measurable whenever a cap is in force. The
|
|
1142
|
+
// model id here matches provider.version (openai-responses-cu resolves the default when unset).
|
|
1143
|
+
const capModelId = config.actors[0]?.model ?? DEFAULT_OPENAI_CU_MODEL;
|
|
1144
|
+
const maxUsd = config.execution?.caps?.maxUsd;
|
|
1130
1145
|
const sessionOptions = {
|
|
1131
1146
|
instructions: spec.instructions,
|
|
1132
1147
|
persona: spec.persona,
|
|
@@ -1135,6 +1150,12 @@ export async function runCuaLane(spec, deps) {
|
|
|
1135
1150
|
apiKey: deps.openaiApiKey,
|
|
1136
1151
|
...(config.actors[0]?.model ? { model: config.actors[0].model } : {})
|
|
1137
1152
|
},
|
|
1153
|
+
...(maxUsd === undefined
|
|
1154
|
+
? {}
|
|
1155
|
+
: {
|
|
1156
|
+
maxUsd,
|
|
1157
|
+
estimateTurnCostUsd: (input, output) => estimateActorCost({ input, output }, capModelId).estimatedCostUsd
|
|
1158
|
+
}),
|
|
1138
1159
|
desktop: desktop,
|
|
1139
1160
|
...(launchedBrowserFamily === "chromium"
|
|
1140
1161
|
? {
|
|
@@ -1220,9 +1241,22 @@ export async function runCuaLane(spec, deps) {
|
|
|
1220
1241
|
else {
|
|
1221
1242
|
warnings.push("Installed @e2b/desktop SDK does not expose Sandbox.kill; server-side kill-on-timeout will reclaim the sandbox.");
|
|
1222
1243
|
}
|
|
1244
|
+
// Close the billed span for BOTH the killed and kept-for-debug paths (a kept sandbox is
|
|
1245
|
+
// still billed until its server-side timeout, so the honest span ends here either way).
|
|
1246
|
+
sandboxTornDownAtMs = deps.now();
|
|
1223
1247
|
}
|
|
1224
1248
|
}
|
|
1249
|
+
// Host-side approximation of the E2B desktop's billed lifetime; feeds the desktop-minute cost
|
|
1250
|
+
// estimate. Never negative.
|
|
1251
|
+
const desktopDurationMs = sandboxCreatedAtMs !== undefined && sandboxTornDownAtMs !== undefined
|
|
1252
|
+
? Math.max(0, sandboxTornDownAtMs - sandboxCreatedAtMs)
|
|
1253
|
+
: undefined;
|
|
1225
1254
|
if (session) {
|
|
1255
|
+
// Per-lane model-token cost ESTIMATE, attached to the trace before it is persisted (the model
|
|
1256
|
+
// id is authoritative here — provider.version). Kept at the lab boundary so the pure loop
|
|
1257
|
+
// never depends on the operator rate table. estimateActorCost declares absent (null) for an
|
|
1258
|
+
// unknown rate / missing usage rather than guessing.
|
|
1259
|
+
session.trace.estimatedCost = estimateActorCost(session.trace.tokenUsage, session.trace.ids.model);
|
|
1226
1260
|
await writeContainedOutputFile(deps.artifactRoot, spec.traceArtifactPath, `${JSON.stringify(session.trace, null, 2)}\n`, "utf8");
|
|
1227
1261
|
if (session.trace.redaction.screenshots === "raw") {
|
|
1228
1262
|
warnings.push("Screenshots are full-fidelity (raw) for local use — the bundle stays in gitignored .humanish and nothing scans these pixels; review them before sharing anywhere. Set policies.redactScreenshots: true to blur a share-as-is bundle.");
|
|
@@ -1249,6 +1283,7 @@ export async function runCuaLane(spec, deps) {
|
|
|
1249
1283
|
...(session ? { session } : {}),
|
|
1250
1284
|
...(sessionError === undefined ? {} : { sessionError }),
|
|
1251
1285
|
...(sandboxId === undefined ? {} : { sandboxId }),
|
|
1286
|
+
...(desktopDurationMs === undefined ? {} : { desktopDurationMs }),
|
|
1252
1287
|
killed,
|
|
1253
1288
|
streamUrlPresent: streamUrl !== undefined,
|
|
1254
1289
|
screenshots,
|
|
@@ -1693,6 +1728,18 @@ export async function runCuaActorLab(options) {
|
|
|
1693
1728
|
if (missingSubjectEnv.length > 0) {
|
|
1694
1729
|
return fail("HUMANISH_CUA_LAB_SUBJECT_ENV_MISSING", `subject.env declares ${missingSubjectEnv.join(", ")} but the environment does not provide ${missingSubjectEnv.length === 1 ? "it" : "them"} (pass via --env-file; values are never persisted).`, descriptor.id);
|
|
1695
1730
|
}
|
|
1731
|
+
// FAIL-CLOSED CAP TENSION (discipline #3): a maxUsd cap needs a MEASURABLE per-turn estimate.
|
|
1732
|
+
// If the operator set execution.caps.maxUsd but src/pricing.ts has no rate for the resolved
|
|
1733
|
+
// model, the loop could not enforce the cap — and silently running uncapped would break the
|
|
1734
|
+
// runaway-retry protection. Refuse at PREFLIGHT (before any sandbox/spend) rather than run
|
|
1735
|
+
// uncapped: an unenforceable cap is more dangerous than none. The operator adds a rate to
|
|
1736
|
+
// src/pricing.ts (the honest place) or removes the cap.
|
|
1737
|
+
if (config.execution?.caps?.maxUsd !== undefined) {
|
|
1738
|
+
const capModelId = (config.actors[0]?.model ?? DEFAULT_OPENAI_CU_MODEL).trim().toLowerCase();
|
|
1739
|
+
if (!MODEL_RATES[capModelId]) {
|
|
1740
|
+
return fail("HUMANISH_CUA_LAB_UNPRICED_CAP", `execution.caps.maxUsd is set but src/pricing.ts has no rate for model "${config.actors[0]?.model ?? DEFAULT_OPENAI_CU_MODEL}"; add a rate or remove the cap — an unenforceable cap is refused rather than run uncapped.`, descriptor.id);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1696
1743
|
}
|
|
1697
1744
|
const runId = options.runId ?? makeCuaRunId();
|
|
1698
1745
|
const runPaths = await prepareRunArtifactPaths(cwd, runId);
|
|
@@ -1757,6 +1804,7 @@ export async function runCuaActorLab(options) {
|
|
|
1757
1804
|
redactScreenshots,
|
|
1758
1805
|
scrubKnownValues,
|
|
1759
1806
|
runSession,
|
|
1807
|
+
now: hooks.now ?? Date.now,
|
|
1760
1808
|
hooks: liveHooks
|
|
1761
1809
|
};
|
|
1762
1810
|
const inProgressLaneSubjects = laneSpecs.map(() => laneSubjectProjection({
|
|
@@ -1856,6 +1904,14 @@ export async function runCuaActorLab(options) {
|
|
|
1856
1904
|
// `first.source !== "clone"` branch below returns it directly, with no unanimity math needed
|
|
1857
1905
|
// (there is nothing that could diverge).
|
|
1858
1906
|
const aggregateWarnings = [];
|
|
1907
|
+
// execution.caps.maxUsd is a PER-LANE cap: it is enforced INSIDE each lane's loop independently,
|
|
1908
|
+
// so an N-lane fan-out can spend up to N × maxUsd before any lane aborts, while the run cost
|
|
1909
|
+
// summary reports the (larger) aggregate. Warn at run level so the operator sees the true ceiling.
|
|
1910
|
+
// A shared, run-level budget is future work — there is no cross-lane counter today.
|
|
1911
|
+
const perLaneCapUsd = config.execution?.caps?.maxUsd;
|
|
1912
|
+
if (perLaneCapUsd !== undefined && laneCount > 1) {
|
|
1913
|
+
aggregateWarnings.push(`execution.caps.maxUsd ($${perLaneCapUsd}) is a PER-LANE cap; ${laneCount} lanes may spend up to ${laneCount} × $${perLaneCapUsd} (~$${round6(perLaneCapUsd * laneCount)} total) before any lane aborts. A shared run-level budget is future work.`);
|
|
1914
|
+
}
|
|
1859
1915
|
const aggregateSubject = (() => {
|
|
1860
1916
|
const first = laneSubjects[0];
|
|
1861
1917
|
if (first.source !== "clone") {
|
|
@@ -2129,6 +2185,9 @@ function buildSingleLaneBundle(args) {
|
|
|
2129
2185
|
}),
|
|
2130
2186
|
...(args.localAppSubject || args.inProcessRoute ? { entryKind: "local-app" } : {}),
|
|
2131
2187
|
...(outcome?.session ? { traceArtifactPath: spec.traceArtifactPath } : {}),
|
|
2188
|
+
...(desktopSpanToMinutes(outcome?.desktopDurationMs) === undefined
|
|
2189
|
+
? {}
|
|
2190
|
+
: { desktopMinutes: desktopSpanToMinutes(outcome?.desktopDurationMs) }),
|
|
2132
2191
|
phaseEvents: outcome?.phaseRecords ?? []
|
|
2133
2192
|
});
|
|
2134
2193
|
}
|
|
@@ -2455,8 +2514,103 @@ function tailOf(log) {
|
|
|
2455
2514
|
* `stream.actor = session.trace` — the provider-neutral ActorTrace seam the Observer renders.
|
|
2456
2515
|
* Exported for the bundle-builder tests.
|
|
2457
2516
|
*/
|
|
2517
|
+
/**
|
|
2518
|
+
* Assemble the run-level cost ESTIMATE from each lane's persisted per-actor estimate
|
|
2519
|
+
* (trace.estimatedCost, set at the lab boundary) plus the aggregated E2B desktop-minute span.
|
|
2520
|
+
* Returns undefined (cost OMITTED) when nothing was priceable AND no sandbox ran — a pure dry-run
|
|
2521
|
+
* or an in-process lane (no trace.estimatedCost, no desktop) stays byte-stable with no cost block.
|
|
2522
|
+
* The null-discipline mirrors the terminal ledger: a present-but-unpriceable line is null + a
|
|
2523
|
+
* reason and contributes NOTHING to estimatedTotalUsd (never coerced to 0); an all-null summary
|
|
2524
|
+
* has a null total. Every non-null figure carries its ratesAsOf date + source (invariant 6).
|
|
2525
|
+
*/
|
|
2526
|
+
export function buildCuaCostSummary(args) {
|
|
2527
|
+
const breakdown = [];
|
|
2528
|
+
let sumInput = 0;
|
|
2529
|
+
let sumOutput = 0;
|
|
2530
|
+
for (const lane of args.lanes) {
|
|
2531
|
+
const usage = lane.trace.tokenUsage;
|
|
2532
|
+
if (usage) {
|
|
2533
|
+
sumInput += usage.input ?? 0;
|
|
2534
|
+
sumOutput += usage.output ?? 0;
|
|
2535
|
+
}
|
|
2536
|
+
const est = lane.trace.estimatedCost;
|
|
2537
|
+
if (!est) {
|
|
2538
|
+
continue;
|
|
2539
|
+
}
|
|
2540
|
+
breakdown.push({
|
|
2541
|
+
kind: "model-tokens",
|
|
2542
|
+
...(lane.laneId === undefined ? {} : { laneId: lane.laneId }),
|
|
2543
|
+
...(est.modelId === undefined ? {} : { modelId: est.modelId }),
|
|
2544
|
+
estimatedCostUsd: est.estimatedCostUsd,
|
|
2545
|
+
...(est.reason === undefined ? {} : { reason: est.reason }),
|
|
2546
|
+
ratesAsOf: est.ratesAsOf,
|
|
2547
|
+
...(est.source === undefined ? {} : { source: est.source }),
|
|
2548
|
+
...(est.placeholder ? { placeholder: true } : {})
|
|
2549
|
+
});
|
|
2550
|
+
}
|
|
2551
|
+
if (args.desktopMinutes !== undefined) {
|
|
2552
|
+
const desktop = estimateDesktopCost(args.desktopMinutes);
|
|
2553
|
+
breakdown.push({
|
|
2554
|
+
kind: "desktop-minutes",
|
|
2555
|
+
estimatedCostUsd: desktop.estimatedCostUsd,
|
|
2556
|
+
...(desktop.reason === undefined ? {} : { reason: desktop.reason }),
|
|
2557
|
+
ratesAsOf: desktop.ratesAsOf,
|
|
2558
|
+
...(desktop.source === undefined ? {} : { source: desktop.source }),
|
|
2559
|
+
...(desktop.placeholder ? { placeholder: true } : {})
|
|
2560
|
+
});
|
|
2561
|
+
}
|
|
2562
|
+
if (breakdown.length === 0) {
|
|
2563
|
+
return undefined;
|
|
2564
|
+
}
|
|
2565
|
+
let knownSum = 0;
|
|
2566
|
+
let anyKnown = false;
|
|
2567
|
+
let anyNull = false;
|
|
2568
|
+
let placeholder = false;
|
|
2569
|
+
// Aggregate freshness is CONSERVATIVE: an aggregate estimate is only as current as its OLDEST
|
|
2570
|
+
// contributing rate, so ratesAsOf takes the MIN (oldest) asOf — MAX would overclaim freshness the
|
|
2571
|
+
// moment operator-edited rates in src/pricing.ts diverge. Each breakdown line keeps its own true asOf.
|
|
2572
|
+
let minRatesAsOf = null;
|
|
2573
|
+
for (const line of breakdown) {
|
|
2574
|
+
if (line.estimatedCostUsd === null) {
|
|
2575
|
+
anyNull = true;
|
|
2576
|
+
continue;
|
|
2577
|
+
}
|
|
2578
|
+
anyKnown = true;
|
|
2579
|
+
knownSum += line.estimatedCostUsd;
|
|
2580
|
+
if (line.placeholder)
|
|
2581
|
+
placeholder = true;
|
|
2582
|
+
if (line.ratesAsOf !== null && (minRatesAsOf === null || line.ratesAsOf < minRatesAsOf)) {
|
|
2583
|
+
minRatesAsOf = line.ratesAsOf;
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
const estimatedTotalUsd = anyKnown ? round6(knownSum) : null;
|
|
2587
|
+
const note = estimatedTotalUsd === null
|
|
2588
|
+
? `No priced spend lines this run — every cost line is DECLARED ABSENT (unknown rate / no usage / no duration); nothing is guessed. Add a rate to src/pricing.ts to estimate this model.`
|
|
2589
|
+
: `Estimated ${estimatedTotalUsd} USD total${anyNull ? " (LOWER BOUND — some lines unmeasured/unpriced)" : ""}${placeholder ? "; includes PLACEHOLDER rate(s) — confirm before trusting the magnitude" : ""}. Every figure is an ESTIMATE (rates as of ${minRatesAsOf} — the OLDEST contributing rate, since an aggregate is only as fresh as its stalest input), a rate-table multiply, NOT an authoritative provider charge.`;
|
|
2590
|
+
return {
|
|
2591
|
+
schema: "humanish.run-cost-summary.v1",
|
|
2592
|
+
currency: "usd",
|
|
2593
|
+
estimatedTotalUsd,
|
|
2594
|
+
ratesAsOf: minRatesAsOf,
|
|
2595
|
+
fullyEstimated: !anyNull,
|
|
2596
|
+
placeholder,
|
|
2597
|
+
breakdown,
|
|
2598
|
+
tokenUsage: { input: sumInput, output: sumOutput, total: sumInput + sumOutput },
|
|
2599
|
+
desktopMinutes: args.desktopMinutes ?? null,
|
|
2600
|
+
note
|
|
2601
|
+
};
|
|
2602
|
+
}
|
|
2603
|
+
// Convert a host-side desktop span (ms) into billed minutes, or undefined when no sandbox ran.
|
|
2604
|
+
function desktopSpanToMinutes(desktopDurationMs) {
|
|
2605
|
+
return desktopDurationMs === undefined ? undefined : desktopDurationMs / 60_000;
|
|
2606
|
+
}
|
|
2458
2607
|
export function buildCuaBundle(args) {
|
|
2459
2608
|
const publicAppUrl = publicSafeAppUrlLabel(args.appUrl);
|
|
2609
|
+
// Run-level cost ESTIMATE (advisory; omitted when nothing was priced and no sandbox ran).
|
|
2610
|
+
const cost = buildCuaCostSummary({
|
|
2611
|
+
lanes: args.session ? [{ ...(args.laneId === undefined ? {} : { laneId: args.laneId }), trace: args.session.trace }] : [],
|
|
2612
|
+
desktopMinutes: args.desktopMinutes
|
|
2613
|
+
});
|
|
2460
2614
|
const status = args.inProgress === true
|
|
2461
2615
|
? "running"
|
|
2462
2616
|
: args.session
|
|
@@ -2728,7 +2882,8 @@ export function buildCuaBundle(args) {
|
|
|
2728
2882
|
// honest on app-url bundles too — the caller minted the URL, its state is the caller's.
|
|
2729
2883
|
// CuaSubjectProvenanceArg's two variants (clone, local-tree) are already RunSubjectProvenance-
|
|
2730
2884
|
// shaped, so no reconstruction is needed beyond the app-url fallback.
|
|
2731
|
-
subject: args.subjectProvenance ?? { source: "app-url", state: { provenance: "undeclared" } }
|
|
2885
|
+
subject: args.subjectProvenance ?? { source: "app-url", state: { provenance: "undeclared" } },
|
|
2886
|
+
...(cost === undefined ? {} : { cost })
|
|
2732
2887
|
};
|
|
2733
2888
|
}
|
|
2734
2889
|
/** Human-readable provenance line for the single-lane subject.provenance event (invariant 5):
|
|
@@ -3086,6 +3241,17 @@ export function buildCuaFanoutBundle(args) {
|
|
|
3086
3241
|
streamId: outcome.spec.streamId,
|
|
3087
3242
|
laneId: outcome.spec.laneId
|
|
3088
3243
|
}));
|
|
3244
|
+
// Run-level cost ESTIMATE: one model-token line per lane that ran a session (from its persisted
|
|
3245
|
+
// trace.estimatedCost) + one aggregate desktop-minutes line summing each lane's OWN sandbox span
|
|
3246
|
+
// (per-lane worlds => no shared provisioning to double-count). Omitted on a pure dry-run.
|
|
3247
|
+
const costLanes = specs
|
|
3248
|
+
.map((spec, index) => ({ laneId: spec.laneId, outcome: outcomes?.[index] }))
|
|
3249
|
+
.filter((entry) => entry.outcome?.session !== undefined)
|
|
3250
|
+
.map((entry) => ({ laneId: entry.laneId, trace: entry.outcome.session.trace }));
|
|
3251
|
+
const desktopMinutesTotal = (outcomes ?? []).some((outcome) => outcome.desktopDurationMs !== undefined)
|
|
3252
|
+
? (outcomes ?? []).reduce((sum, outcome) => sum + (outcome.desktopDurationMs ?? 0), 0) / 60_000
|
|
3253
|
+
: undefined;
|
|
3254
|
+
const cost = buildCuaCostSummary({ lanes: costLanes, desktopMinutes: desktopMinutesTotal });
|
|
3089
3255
|
return {
|
|
3090
3256
|
schema: RUN_BUNDLE_SCHEMA,
|
|
3091
3257
|
runId: args.runId,
|
|
@@ -3142,7 +3308,8 @@ export function buildCuaFanoutBundle(args) {
|
|
|
3142
3308
|
? {}
|
|
3143
3309
|
: { desktopBrowser: { requested: configuredBrowser, ...(unanimousResolvedBrowser === undefined ? {} : { resolved: unanimousResolvedBrowser }) } }),
|
|
3144
3310
|
...(providerResources.length === 0 ? {} : { providerResources }),
|
|
3145
|
-
subject: args.aggregateSubject
|
|
3311
|
+
subject: args.aggregateSubject,
|
|
3312
|
+
...(cost === undefined ? {} : { cost })
|
|
3146
3313
|
};
|
|
3147
3314
|
}
|
|
3148
3315
|
function providerResourcesForOutcome(args) {
|