humanish 0.145.0 → 0.146.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/CHANGELOG.md +11 -0
- package/dist/analysis/admission.d.ts +8 -3
- package/dist/analysis/admission.js +7 -3
- package/dist/analysis/automatic-config.js +2 -1
- package/dist/analysis/automatic.js +2 -2
- package/dist/analysis/execute.d.ts +7 -10
- package/dist/analysis/execute.js +79 -49
- package/dist/analysis/output-limit.d.ts +12 -0
- package/dist/analysis/output-limit.js +34 -0
- package/dist/analysis/output-retry.d.ts +20 -0
- package/dist/analysis/output-retry.js +63 -0
- package/dist/analysis/provider.d.ts +1 -1
- package/dist/analysis/provider.js +3 -3
- package/dist/analysis/responses.js +1 -1
- package/dist/analysis/service.js +2 -2
- package/dist/cli/commands/analyze.js +1 -1
- package/dist/cli/io.js +2 -0
- package/dist/cli/run-findings.js +10 -2
- package/dist/observer-app.html +1 -1
- package/dist/remote/launch.js +84 -44
- package/dist/remote/runner.d.ts +47 -14
- package/dist/remote/runner.js +199 -64
- package/dist/routes/computer-use/e2b-desktop/desktop.js +47 -7
- package/dist/routes/computer-use/e2b-desktop/prepare.d.ts +4 -2
- package/dist/routes/computer-use/e2b-desktop/prepare.js +4 -2
- package/dist/routes/computer-use/e2b-desktop/start.d.ts +14 -6
- package/dist/routes/computer-use/e2b-desktop/start.js +72 -34
- package/dist/substrates/e2b/cdp-probe.d.ts +16 -1
- package/dist/substrates/e2b/cdp-probe.js +67 -5
- package/dist/substrates/e2b/desktop-browser.js +4 -2
- package/dist/substrates/e2b/desktop-cdp.d.ts +9 -2
- package/dist/substrates/e2b/desktop-cdp.js +26 -4
- package/docs/contracts/schemas.md +1 -1
- package/docs/contracts/study-analysis.md +62 -9
- package/docs/product/automatic-analysis.md +9 -6
- package/package.json +5 -5
|
@@ -30,8 +30,11 @@ export interface ChromeCdpProbeArgs {
|
|
|
30
30
|
* pausing it; it applies `emulation` (how a participant keeps the overrides for its whole life,
|
|
31
31
|
* with a reload on each page) and `network` capture, either or both. "fidelity": read
|
|
32
32
|
* back what the page reports about itself (UA, DPR, viewport, touch), the proof for the bundle.
|
|
33
|
+
* "load": poll the page until it has loaded and painted, or until `deadlineMs`.
|
|
33
34
|
*/
|
|
34
|
-
mode: "state" | "geometry" | "port" | "emulate" | "hold" | "fidelity";
|
|
35
|
+
mode: "state" | "geometry" | "port" | "emulate" | "hold" | "fidelity" | "load";
|
|
36
|
+
/** For "load": how long to poll before reporting the page as not loaded. */
|
|
37
|
+
deadlineMs?: number;
|
|
35
38
|
/** For "emulate" and "hold": what to apply. */
|
|
36
39
|
emulation?: ChromeMobileEmulationRequest;
|
|
37
40
|
/** For "hold": capture each held page's requests to a sandbox file. */
|
|
@@ -41,6 +44,16 @@ export interface ChromeCdpProbeArgs {
|
|
|
41
44
|
export interface ChromeMobileEmulationRequest extends MobileEmulationRequest {
|
|
42
45
|
platform?: string;
|
|
43
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* What the "load" mode saw. `loaded` is true once the page reports document.readyState complete,
|
|
49
|
+
* is visible and has a first contentful paint. `seen` describes the last read in words that name
|
|
50
|
+
* no URL.
|
|
51
|
+
*/
|
|
52
|
+
export interface ChromePageLoad {
|
|
53
|
+
loaded: boolean;
|
|
54
|
+
waitedMs: number;
|
|
55
|
+
seen: string;
|
|
56
|
+
}
|
|
44
57
|
/** What the page reports after emulation: the proof, never copied from the request. */
|
|
45
58
|
interface ChromeFidelityRead {
|
|
46
59
|
userAgent: string;
|
|
@@ -80,6 +93,8 @@ export interface ChromeCdpProbeResult {
|
|
|
80
93
|
} | {
|
|
81
94
|
unavailable: string;
|
|
82
95
|
};
|
|
96
|
+
/** "load": whether the page loaded and painted before the deadline, and what the last read saw. */
|
|
97
|
+
load?: ChromePageLoad;
|
|
83
98
|
/**
|
|
84
99
|
* Seconds since the epoch on the sandbox clock when the probe printed, on every answer including
|
|
85
100
|
* an unavailable one. It is the clock CDP `wallTime` uses, so a turn mark compares with request
|
|
@@ -213,6 +213,10 @@ GEOMETRY_EXPRESSION = (
|
|
|
213
213
|
"({ browserWindow: { x: window.screenX, y: window.screenY, width: window.outerWidth, height: window.outerHeight }, "
|
|
214
214
|
"viewport: { width: window.innerWidth, height: window.innerHeight, deviceScaleFactor: window.devicePixelRatio } })"
|
|
215
215
|
)
|
|
216
|
+
LOAD_EXPRESSION = (
|
|
217
|
+
"({ readyState: document.readyState, visible: document.visibilityState === 'visible', "
|
|
218
|
+
"painted: performance.getEntriesByType('paint').some(function (entry) { return entry.name === 'first-contentful-paint'; }) })"
|
|
219
|
+
)
|
|
216
220
|
FIDELITY_EXPRESSION = (
|
|
217
221
|
"({ userAgent: navigator.userAgent, devicePixelRatio: window.devicePixelRatio, "
|
|
218
222
|
"innerWidth: window.innerWidth, innerHeight: window.innerHeight, "
|
|
@@ -405,6 +409,50 @@ def follow_targets(ws, launch_id, request, log):
|
|
|
405
409
|
if params.get("waitingForDebugger"):
|
|
406
410
|
ws.send("Runtime.runIfWaitingForDebugger", {}, new_session)
|
|
407
411
|
|
|
412
|
+
def page_ws_url(port, page):
|
|
413
|
+
# Chrome omits webSocketDebuggerUrl from /json while another DevTools client is attached to the
|
|
414
|
+
# page, or for a moment after one detaches; the page socket URL is still /devtools/page/<id>.
|
|
415
|
+
return page.get("webSocketDebuggerUrl") or (
|
|
416
|
+
"ws://127.0.0.1:%d/devtools/page/%s" % (port, page.get("id")) if page.get("id") else None
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
def wait_for_load(port, args):
|
|
420
|
+
"""Poll the page until document.readyState is complete, the page is visible and Chrome has
|
|
421
|
+
recorded its first contentful paint: the page has loaded and something of it is drawn. Stops at
|
|
422
|
+
deadlineMs and reports what the last read saw."""
|
|
423
|
+
started = time.monotonic()
|
|
424
|
+
deadline_ms = int(args.get("deadlineMs") or 0)
|
|
425
|
+
seen = "no http page yet"
|
|
426
|
+
page = None
|
|
427
|
+
loaded = False
|
|
428
|
+
while True:
|
|
429
|
+
try:
|
|
430
|
+
page = select_page(list_pages(port), args)
|
|
431
|
+
seen = "no http page yet" if page is None else seen
|
|
432
|
+
except Exception as error:
|
|
433
|
+
page, seen = None, "the DevTools endpoint did not answer (%s)" % type(error).__name__
|
|
434
|
+
ws_url = page_ws_url(port, page) if page is not None else None
|
|
435
|
+
state = evaluate(ws_url, LOAD_EXPRESSION) if ws_url else None
|
|
436
|
+
if isinstance(state, dict):
|
|
437
|
+
ready = str(state.get("readyState") or "unknown")
|
|
438
|
+
seen = "document.readyState %s, %s, %s" % (
|
|
439
|
+
ready,
|
|
440
|
+
"visible" if state.get("visible") else "hidden",
|
|
441
|
+
"first contentful paint recorded" if state.get("painted") else "no first contentful paint yet",
|
|
442
|
+
)
|
|
443
|
+
loaded = ready == "complete" and bool(state.get("visible")) and bool(state.get("painted"))
|
|
444
|
+
if loaded:
|
|
445
|
+
break
|
|
446
|
+
elif ws_url:
|
|
447
|
+
seen = "the page did not answer Runtime.evaluate"
|
|
448
|
+
if (time.monotonic() - started) * 1000 >= deadline_ms:
|
|
449
|
+
break
|
|
450
|
+
time.sleep(0.25)
|
|
451
|
+
answer = {"load": {"loaded": loaded, "waitedMs": int((time.monotonic() - started) * 1000), "seen": seen}}
|
|
452
|
+
if page is not None:
|
|
453
|
+
answer["targetId"] = str(page.get("id") or "")
|
|
454
|
+
emit(answer)
|
|
455
|
+
|
|
408
456
|
def emit(answer):
|
|
409
457
|
"""Print one answer line stamped with this sandbox's clock. CDP wallTime uses the same clock, so
|
|
410
458
|
a turn mark taken from it compares with request times without host skew, and the reading needs
|
|
@@ -419,6 +467,9 @@ def main():
|
|
|
419
467
|
if mode == "port":
|
|
420
468
|
emit({"cdpPort": port})
|
|
421
469
|
return
|
|
470
|
+
if mode == "load":
|
|
471
|
+
wait_for_load(port, args)
|
|
472
|
+
return
|
|
422
473
|
try:
|
|
423
474
|
pages = list_pages(port)
|
|
424
475
|
except Exception as error:
|
|
@@ -441,11 +492,7 @@ def main():
|
|
|
441
492
|
if page is None:
|
|
442
493
|
emit({"unavailable": "no http page among %d CDP targets on 127.0.0.1:%d" % (len(pages), port)})
|
|
443
494
|
return
|
|
444
|
-
|
|
445
|
-
# page, or for a moment after one detaches; the page socket URL is still /devtools/page/<id>.
|
|
446
|
-
ws_url = page.get("webSocketDebuggerUrl") or (
|
|
447
|
-
"ws://127.0.0.1:%d/devtools/page/%s" % (port, page.get("id")) if page.get("id") else None
|
|
448
|
-
)
|
|
495
|
+
ws_url = page_ws_url(port, page)
|
|
449
496
|
if mode == "emulate":
|
|
450
497
|
applied, failure = emulate(ws_url, args.get("emulation") or {}) if ws_url else (None, "the page has no socket")
|
|
451
498
|
if failure is not None:
|
|
@@ -502,6 +549,8 @@ export function chromeCdpProbeCommand(args) {
|
|
|
502
549
|
payload.emulation = args.emulation;
|
|
503
550
|
if (args.network !== undefined)
|
|
504
551
|
payload.network = args.network;
|
|
552
|
+
if (args.deadlineMs !== undefined)
|
|
553
|
+
payload.deadlineMs = args.deadlineMs;
|
|
505
554
|
return `python3 -c ${shellQuote(CHROME_CDP_PROBE_PY)} ${shellQuote(JSON.stringify(payload))}`;
|
|
506
555
|
}
|
|
507
556
|
/**
|
|
@@ -529,6 +578,7 @@ export function parseChromeCdpProbeOutput(stdout) {
|
|
|
529
578
|
const numberOr = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
530
579
|
const now = numberOr(record.now);
|
|
531
580
|
const network = networkOf(record.network);
|
|
581
|
+
const load = loadOf(record.load);
|
|
532
582
|
if (typeof record.unavailable === "string") {
|
|
533
583
|
return {
|
|
534
584
|
unavailable: record.unavailable,
|
|
@@ -602,9 +652,21 @@ export function parseChromeCdpProbeOutput(stdout) {
|
|
|
602
652
|
...(browserWindow === undefined ? {} : { browserWindow }),
|
|
603
653
|
...(viewport === undefined ? {} : { viewport }),
|
|
604
654
|
...(network === undefined ? {} : { network }),
|
|
655
|
+
...(load === undefined ? {} : { load }),
|
|
605
656
|
...(now === undefined ? {} : { now }),
|
|
606
657
|
};
|
|
607
658
|
}
|
|
659
|
+
function loadOf(value) {
|
|
660
|
+
if (!value || typeof value !== "object")
|
|
661
|
+
return undefined;
|
|
662
|
+
const source = value;
|
|
663
|
+
if (typeof source.loaded !== "boolean" ||
|
|
664
|
+
typeof source.waitedMs !== "number" ||
|
|
665
|
+
!Number.isFinite(source.waitedMs) ||
|
|
666
|
+
typeof source.seen !== "string")
|
|
667
|
+
return undefined;
|
|
668
|
+
return { loaded: source.loaded, waitedMs: source.waitedMs, seen: source.seen };
|
|
669
|
+
}
|
|
608
670
|
function networkOf(value) {
|
|
609
671
|
if (!value || typeof value !== "object")
|
|
610
672
|
return undefined;
|
|
@@ -7,8 +7,10 @@ import { runDetachedStep } from "../detached.js";
|
|
|
7
7
|
import { shellQuote } from "../shell.js";
|
|
8
8
|
import { externalLinkHandlerSetup } from "./desktop-handoff.js";
|
|
9
9
|
import { e2bShell } from "./shell.js";
|
|
10
|
-
//
|
|
11
|
-
// a cold browser and page load to paint (2 s captured a
|
|
10
|
+
// The wait before the first screenshot. After a terminal opens, or a browser without a DevTools
|
|
11
|
+
// read, it is a fixed sleep: long enough for a cold browser and page load to paint (2 s captured a
|
|
12
|
+
// blank desktop; the render needs 6-9 s). A Chromium participant waits for its page to load and
|
|
13
|
+
// paint (waitForChromePageLoad) and gives up after this long.
|
|
12
14
|
export const DESKTOP_SETTLE_MS = 8_000;
|
|
13
15
|
/** The DevTools port every Chromium participant launches with; see `chrome_debug_flags` below. */
|
|
14
16
|
export const CHROME_DEVTOOLS_PORT = 9222;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ChromeNetworkCaptureRequest } from "./cdp-network-capture.js";
|
|
2
|
-
import { type ChromeCdpProbeResult, type ChromeMobileEmulationRequest } from "./cdp-probe.js";
|
|
2
|
+
import { type ChromeCdpProbeResult, type ChromeMobileEmulationRequest, type ChromePageLoad } from "./cdp-probe.js";
|
|
3
3
|
import type { RunDesktopGeometry } from "../../run/streams.js";
|
|
4
4
|
import type { E2BDesktopSandbox } from "./sdk.js";
|
|
5
5
|
/**
|
|
@@ -85,6 +85,13 @@ export declare function applyMobileEmulation(desktop: E2BDesktopSandbox, request
|
|
|
85
85
|
/** The holder's capture status, when `network` was passed and the holder said. */
|
|
86
86
|
network?: ChromeCdpProbeResult["network"];
|
|
87
87
|
}>;
|
|
88
|
+
/**
|
|
89
|
+
* Wait until the participant's launch page has loaded and painted, read over DevTools in the
|
|
90
|
+
* sandbox, for at most `deadlineMs`. Resolves with what the wait saw, or undefined when it could
|
|
91
|
+
* not run (no python3, a failed command, output that is not the probe's), so the caller can fall
|
|
92
|
+
* back to a fixed wait.
|
|
93
|
+
*/
|
|
94
|
+
export declare function waitForChromePageLoad(desktop: E2BDesktopSandbox, requestTimeoutMs: number, endpoint: ChromeCdpEndpoint, deadlineMs: number): Promise<ChromePageLoad | undefined>;
|
|
88
95
|
/**
|
|
89
96
|
* Start a holder that records the launch page's requests, and every later tab's, to
|
|
90
97
|
* `network.eventsPath`, for a participant without mobile emulation; applyMobileEmulation passes
|
|
@@ -94,7 +101,7 @@ export declare function applyMobileEmulation(desktop: E2BDesktopSandbox, request
|
|
|
94
101
|
export declare function startNetworkCapture(desktop: E2BDesktopSandbox, requestTimeoutMs: number, endpoint: ChromeCdpEndpoint, network: ChromeNetworkCaptureRequest): Promise<string>;
|
|
95
102
|
/**
|
|
96
103
|
* What the capture holder announced: when capture started on the launch page, on the sandbox
|
|
97
|
-
* clock, or why it did not. Reads the holder's log up to
|
|
104
|
+
* clock, or why it did not. Reads the holder's log up to 22 times, half a second apart by
|
|
98
105
|
* `desktop.wait`, and reports a holder that printed nothing as unavailable.
|
|
99
106
|
*/
|
|
100
107
|
export declare function readNetworkCaptureStart(desktop: E2BDesktopSandbox, requestTimeoutMs: number, holderName: string): Promise<NonNullable<ChromeCdpProbeResult["network"]>>;
|
|
@@ -213,10 +213,32 @@ export async function applyMobileEmulation(desktop, requestTimeoutMs, endpoint,
|
|
|
213
213
|
...capture,
|
|
214
214
|
};
|
|
215
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Wait until the participant's launch page has loaded and painted, read over DevTools in the
|
|
218
|
+
* sandbox, for at most `deadlineMs`. Resolves with what the wait saw, or undefined when it could
|
|
219
|
+
* not run (no python3, a failed command, output that is not the probe's), so the caller can fall
|
|
220
|
+
* back to a fixed wait.
|
|
221
|
+
*/
|
|
222
|
+
export async function waitForChromePageLoad(desktop, requestTimeoutMs, endpoint, deadlineMs) {
|
|
223
|
+
const result = await e2bShell(desktop)
|
|
224
|
+
.run(chromeCdpProbeCommand({ ...endpoint, prefer: "pinned", mode: "load", deadlineMs }), {
|
|
225
|
+
requestTimeoutMs,
|
|
226
|
+
// One poll can take 3.5 s past the deadline: a 2 s listing and a 1.5 s evaluate.
|
|
227
|
+
timeoutMs: deadlineMs + 10_000,
|
|
228
|
+
})
|
|
229
|
+
.catch(() => undefined);
|
|
230
|
+
if (result === undefined || result.exitCode !== 0)
|
|
231
|
+
return undefined;
|
|
232
|
+
return parseChromeCdpProbeOutput(result.stdout).load;
|
|
233
|
+
}
|
|
216
234
|
/** The holder stops writing its events file at 32 MiB, so a read stops there too. */
|
|
217
235
|
const NETWORK_EVENTS_MAX_BYTES = 32 * 1024 * 1024;
|
|
218
|
-
/**
|
|
219
|
-
|
|
236
|
+
/**
|
|
237
|
+
* How many times the capture holder's log is read for its announce, and how far apart. The reads
|
|
238
|
+
* start after the page wait, which can end within a second of the holder's start, so they cover
|
|
239
|
+
* the 11 s a holder had when a fixed 8 s settle came before 3 s of reads.
|
|
240
|
+
*/
|
|
241
|
+
const NETWORK_ANNOUNCE_READS = 22;
|
|
220
242
|
const NETWORK_ANNOUNCE_EVERY_MS = 500;
|
|
221
243
|
/**
|
|
222
244
|
* Start a holder that records the launch page's requests, and every later tab's, to
|
|
@@ -235,7 +257,7 @@ export async function startNetworkCapture(desktop, requestTimeoutMs, endpoint, n
|
|
|
235
257
|
}
|
|
236
258
|
/**
|
|
237
259
|
* What the capture holder announced: when capture started on the launch page, on the sandbox
|
|
238
|
-
* clock, or why it did not. Reads the holder's log up to
|
|
260
|
+
* clock, or why it did not. Reads the holder's log up to 22 times, half a second apart by
|
|
239
261
|
* `desktop.wait`, and reports a holder that printed nothing as unavailable.
|
|
240
262
|
*/
|
|
241
263
|
export async function readNetworkCaptureStart(desktop, requestTimeoutMs, holderName) {
|
|
@@ -253,7 +275,7 @@ export async function readNetworkCaptureStart(desktop, requestTimeoutMs, holderN
|
|
|
253
275
|
});
|
|
254
276
|
}
|
|
255
277
|
return {
|
|
256
|
-
unavailable: `the in-sandbox holder printed nothing within ${(NETWORK_ANNOUNCE_READS * NETWORK_ANNOUNCE_EVERY_MS) / 1000} s of the
|
|
278
|
+
unavailable: `the in-sandbox holder printed nothing within ${(NETWORK_ANNOUNCE_READS * NETWORK_ANNOUNCE_EVERY_MS) / 1000} s of the page wait`,
|
|
257
279
|
};
|
|
258
280
|
}
|
|
259
281
|
/**
|
|
@@ -212,7 +212,7 @@ The rest of this section names keys by their v3 spelling:
|
|
|
212
212
|
browser target; `entry` is a shared-world participant's same-origin path;
|
|
213
213
|
- `participants[].files` (computer-use on `execution.target: e2b-desktop` only):
|
|
214
214
|
1 to 20 `{ from, as }` entries, each a file copied onto that participant's desktop
|
|
215
|
-
before its
|
|
215
|
+
before its session starts. `from` is relative to the folder that holds the study file
|
|
216
216
|
(`cwd` for a library config); `as` is relative to the desktop user's home folder.
|
|
217
217
|
The parser refuses an absolute path, a `.`, `..` or empty part, a backslash, an `as`
|
|
218
218
|
that starts with `~`, lands in `subject/` or on a name starting with `.humanish`, looks
|
|
@@ -27,8 +27,10 @@ cap; a refused dry run says why, and names the `--max-cost` that admits it.
|
|
|
27
27
|
|
|
28
28
|
The default provider is `openai`; its default model is `gpt-6.1-sol`, at medium reasoning effort
|
|
29
29
|
([reasoning effort](#reasoning-effort)). A request sends selected retained text and
|
|
30
|
-
captures to OpenAI, without tools, redirects
|
|
31
|
-
|
|
30
|
+
captures to OpenAI, without tools, redirects or provider-side response storage. A request that
|
|
31
|
+
runs out of output is sent once more while the cap admits it
|
|
32
|
+
([a request that runs out of output](#a-request-that-runs-out-of-output)); no other failure is
|
|
33
|
+
sent again. `--question` adds a reviewer question; it never changes the
|
|
32
34
|
participant assignment. For OpenAI API analysis, `--max-cost` is required, including for dry-run
|
|
33
35
|
admission. Admission refuses the request when both its worst case and its expected cost plus a
|
|
34
36
|
10% margin are over it; the provider bill is not limited by it.
|
|
@@ -60,12 +62,39 @@ times for those billed $1 or more.
|
|
|
60
62
|
A refusal's message gives the expected cost, the worst case, the cap, and
|
|
61
63
|
`humanish analyze --run <id> --max-cost <n>` with `n` the worst case rounded up.
|
|
62
64
|
|
|
63
|
-
The default deadline is ten minutes for each request. With no explicit output-token limit,
|
|
64
|
-
|
|
65
|
-
16,
|
|
66
|
-
|
|
67
|
-
and
|
|
68
|
-
|
|
65
|
+
The default deadline is ten minutes for each request. With no explicit output-token limit,
|
|
66
|
+
`src/analysis/output-limit.ts` sizes the allowance to the packet. An OpenAI analysis at none, low
|
|
67
|
+
or medium effort of up to 16 participants gets 12,288 tokens plus 2,048 per participant, at most
|
|
68
|
+
32,768: 14,336 for one participant and 18,432 for three. A high-effort analysis, a Gemini
|
|
69
|
+
analysis and a run analysed in cohorts get 32,768. If admission refuses an allowance above
|
|
70
|
+
16,384 under the declared budget, the analysis keeps 16,384. Explicit limits are never adjusted.
|
|
71
|
+
Selection happens before dispatch; `admission.outputTokenAllowance` exposes it in dry-run, and
|
|
72
|
+
the saved configuration, digest and automatic job bind the selected allowance. This selection
|
|
73
|
+
never increases the spending limit.
|
|
74
|
+
|
|
75
|
+
The sizes come from the finished reports on the 2026-10-22 and 2026-10-23 benchmark recordings:
|
|
76
|
+
the Taskly analyses listed in `docs/evidence/benchmark/2026-10-2*.json`, one participant with 8 to
|
|
77
|
+
14 captures, and the movie-game analyses of
|
|
78
|
+
[analysis models on fixed recordings](https://github.com/danielgwilson/humanish/blob/main/docs/evidence/benchmark/2026-10-22-analysis-models.md), three
|
|
79
|
+
participants with 40 to 49 captures. Each analysis's output tokens are read from its retained
|
|
80
|
+
artifact.
|
|
81
|
+
|
|
82
|
+
| Analyst and effort | One participant: passed, most output | Three participants: passed, most output |
|
|
83
|
+
| -------------------- | ------------------------------------ | --------------------------------------- |
|
|
84
|
+
| `gpt-6.1-sol` low | 24, 4,308 | 6, 6,391 |
|
|
85
|
+
| `gpt-6.1-sol` medium | 142, 5,659 | 36, 9,000 |
|
|
86
|
+
| `gpt-6-luna` medium | 27, 8,354 | 2, 11,313 |
|
|
87
|
+
| `gpt-6.1-sol` high | 22, 10,086 | 6, 15,797 |
|
|
88
|
+
| `gpt-6-luna` high | 78, 19,880 | 9, 26,439 |
|
|
89
|
+
| `gemini-3.8-flash` | 23, 29,739 | 1, 26,154 |
|
|
90
|
+
|
|
91
|
+
At low and medium effort the sized limit is 1.7 times the most output of a finished report of
|
|
92
|
+
one participant and 1.6 times that of three; for `gpt-6.1-sol` at medium it is 2.5 and 2.0 times.
|
|
93
|
+
At high effort a finished `gpt-6-luna` report of three participants used 26,439 tokens, so 32,768
|
|
94
|
+
has no room to lower. A `gpt-6.1-sol` analysis at medium that ran out of output on a
|
|
95
|
+
one-participant run had used all 32,768 tokens in 374 seconds and cost $0.39; at 14,336 tokens the
|
|
96
|
+
same request stops at 44% of that output.
|
|
97
|
+
|
|
69
98
|
The configured wall-clock deadline covers the entire request, including waiting
|
|
70
99
|
for response headers and reading the body. A request-scoped dispatcher prevents
|
|
71
100
|
Node's separate fetch timeout from cutting a longer configured deadline short;
|
|
@@ -77,6 +106,29 @@ success. Findings keep reported concerns and observed recovery distinct across
|
|
|
77
106
|
participants. Other supported models can be selected explicitly, but evidence
|
|
78
107
|
reference validation does not certify their interpretation of small visual details.
|
|
79
108
|
|
|
109
|
+
## A request that runs out of output
|
|
110
|
+
|
|
111
|
+
When an OpenAI or Gemini request ends with `output_incomplete`, humanish sends the same request
|
|
112
|
+
once more, with a fresh deadline. It does so only when the spend so far, plus what admission
|
|
113
|
+
holds for the attempt's requests still running and admission's figure for one more request (its
|
|
114
|
+
expected cost plus 10%, or its worst case when that is lower), is within `maxCostUsd`. The spend
|
|
115
|
+
so far counts every response of the attempt, the one that ran out included. When a response did
|
|
116
|
+
not report its usage, the spend is unknown and the request is not sent again. A request is sent
|
|
117
|
+
again at most once; when the second one also runs out, the attempt fails with
|
|
118
|
+
`analysis_output_incomplete`. A cohort request and the merge request are each sent again the same
|
|
119
|
+
way. Codex account analysis has no dollar cap to check and is never sent again. A timeout, an HTTP
|
|
120
|
+
error and a rejected report are not sent again: a timed-out request has already used the
|
|
121
|
+
ten-minute deadline.
|
|
122
|
+
|
|
123
|
+
The attempt keeps one execution start, one receipt and one `analysis.json`. Their `usage` is the
|
|
124
|
+
sum of every response, both requests of a retry included, and `estimatedCostUsd` prices each
|
|
125
|
+
response as its own request. The schemas are unchanged. A warning in `warnings` (and in the human
|
|
126
|
+
output of `analyze` and of a live run, under its `analysis:` line) names the request, the input
|
|
127
|
+
and output tokens and the cost of the response that ran out, and says whether humanish sent it
|
|
128
|
+
again; a refused retry gives the spend, the held amount and the cap. The result is partial with
|
|
129
|
+
`analysis_admission_estimate_exceeded` only when the bill passes admission's worst case plus the
|
|
130
|
+
worst case of each request sent again, or the cap.
|
|
131
|
+
|
|
80
132
|
## Runs with more than 16 participants
|
|
81
133
|
|
|
82
134
|
One analysis covers up to 128 participants, the most streams it reads from a run
|
|
@@ -104,7 +156,8 @@ whole packet as a single request's report does.
|
|
|
104
156
|
The attempt has one execution start, one receipt and one `analysis.json`, with the
|
|
105
157
|
run's participants, coverage and evidence and the summed usage of every request
|
|
106
158
|
sent. The artifact schema is unchanged. Its stored limits grew to hold eight
|
|
107
|
-
cohorts: 6,400 evidence items, 320 captures and 16 MiB.
|
|
159
|
+
cohorts: 6,400 evidence items, 320 captures and 16 MiB. A cohort request or the merge request that
|
|
160
|
+
runs out of output is sent once more under the rule above. If a cohort request fails
|
|
108
161
|
or is rejected, no further request starts, requests already sent finish, no merge
|
|
109
162
|
request is sent and the attempt fails with that request's code, with no findings;
|
|
110
163
|
a warning names the cohort's size. A failed merge request fails the attempt the same way. The usage
|
|
@@ -16,15 +16,17 @@ expected cost and a 600-second timeout. A study or command that names a model bu
|
|
|
16
16
|
that model at high; only the default analyst, with no model named, runs at medium. On the same
|
|
17
17
|
recordings it matched `gpt-6-astra` at high, the earlier default, at about one seventh of the cost
|
|
18
18
|
([analysis models on fixed recordings](https://github.com/danielgwilson/humanish/blob/main/docs/evidence/benchmark/2026-10-22-analysis-models.md)). When no output limit is specified,
|
|
19
|
-
humanish
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
humanish sizes it to the analysis: at low or medium effort, 12,288 tokens plus 2,048 per
|
|
20
|
+
participant (14,336 for one), and 32,768 at high effort, on Gemini and past 16 participants. An
|
|
21
|
+
allowance above 16,384 that the cap would refuse falls back to 16,384, so a study its cap admitted
|
|
22
|
+
before stays admitted ([output limit](../contracts/study-analysis.md#invocation)). A
|
|
23
|
+
request that runs out of output is sent once more while the cap admits it. To customize it:
|
|
22
24
|
|
|
23
25
|
```yaml
|
|
24
26
|
review:
|
|
25
27
|
analysis:
|
|
26
28
|
maxCostUsd: 10
|
|
27
|
-
# Optional overrides. Omit maxOutputTokens for
|
|
29
|
+
# Optional overrides. Omit maxOutputTokens for a limit sized to the analysis.
|
|
28
30
|
# A named model without reasoningEffort runs at high.
|
|
29
31
|
model: gpt-6.1-sol
|
|
30
32
|
reasoningEffort: medium
|
|
@@ -57,8 +59,9 @@ effort; on 24 Taskly analyses `gpt-6.1-sol` at medium wrote a mean 3,747 and cos
|
|
|
57
59
|
evidence to inspect the expected cost, the worst case and the selected token
|
|
58
60
|
allowance before deliberately choosing a larger budget. Explicit
|
|
59
61
|
`maxOutputTokens` and `--max-output-tokens` limits are honored exactly. The output allowance
|
|
60
|
-
includes reasoning as well as the report;
|
|
61
|
-
|
|
62
|
+
includes reasoning as well as the report; a request that exhausts it has no usable report. It
|
|
63
|
+
is sent once more when the spend so far and admission's figure for one more request fit the cap,
|
|
64
|
+
and the CLI says so under the analysis line; the attempt's usage counts both requests. Analysis
|
|
62
65
|
sends selected retained text and captures to OpenAI using `OPENAI_API_KEY`, or to Google with
|
|
63
66
|
`provider: gemini`.
|
|
64
67
|
Analysis runs in the humanish runner using its credentials. This setting adds no
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "humanish",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.146.0",
|
|
4
4
|
"description": "Synthetic user research for apps, CLIs, and agent-facing product flows.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -120,13 +120,13 @@
|
|
|
120
120
|
"@types/pngjs": "^6.0.5",
|
|
121
121
|
"axe-core": "4.14.0",
|
|
122
122
|
"fast-check": "catalog:",
|
|
123
|
-
"knip": "6.
|
|
123
|
+
"knip": "6.42.1",
|
|
124
124
|
"modal": "^0.11.1",
|
|
125
125
|
"oxc-parser": "catalog:",
|
|
126
|
-
"oxfmt": "0.
|
|
127
|
-
"oxlint": "1.
|
|
126
|
+
"oxfmt": "0.74.0",
|
|
127
|
+
"oxlint": "1.89.0",
|
|
128
128
|
"oxlint-tsgolint": "7.0.2004",
|
|
129
|
-
"skills": "1.7.
|
|
129
|
+
"skills": "1.7.4",
|
|
130
130
|
"tsx": "^4.23.15",
|
|
131
131
|
"typescript": "catalog:",
|
|
132
132
|
"vitest": "catalog:"
|