humanish 0.127.0 → 0.128.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 +24 -0
- package/CONTEXT.md +2 -2
- package/README.md +1 -1
- package/dist/actors/closing-report.d.ts +8 -1
- package/dist/actors/closing-report.js +4 -1
- package/dist/actors/codex/restricted-participant-policy.js +2 -5
- package/dist/actors/computer-use/loop/debrief.js +6 -5
- package/dist/actors/computer-use/loop/ending.d.ts +5 -4
- package/dist/actors/computer-use/loop/ending.js +5 -4
- package/dist/actors/computer-use/loop/output-limit.js +3 -0
- package/dist/actors/computer-use/loop/types.d.ts +16 -10
- package/dist/actors/computer-use/loop.js +8 -3
- package/dist/actors/contract.d.ts +10 -6
- package/dist/cli/commands/study-format.js +13 -2
- package/dist/feedback/export.js +3 -2
- package/dist/observer/data.d.ts +13 -6
- package/dist/observer/data.js +40 -10
- package/dist/observer-app.html +8 -8
- package/dist/routes/computer-use/fanout-bundle.js +25 -6
- package/dist/routes/computer-use/participant-model.js +1 -1
- package/dist/run/bundle.d.ts +7 -4
- package/dist/run/bundle.js +10 -4
- package/dist/run/detail.js +8 -2
- package/dist/run/locate.d.ts +2 -2
- package/dist/run/locate.js +2 -2
- package/dist/run/run.js +11 -3
- package/dist/run/stream-trace.d.ts +17 -7
- package/dist/run/stream-trace.js +135 -29
- package/dist/run/streams.d.ts +4 -1
- package/dist/study/bundle-estimate.js +17 -17
- package/dist/verify/costs.d.ts +7 -3
- package/dist/verify/costs.js +27 -12
- package/dist/verify/verify.js +14 -8
- package/docs/architecture/actor-contract.md +7 -3
- package/docs/architecture/github-feedback-loop.md +1 -1
- package/docs/architecture/observer.md +19 -10
- package/docs/contracts/README.md +1 -1
- package/docs/contracts/run-bundle.md +93 -15
- package/docs/contracts/schemas.md +55 -45
- package/docs/contracts/study-analysis.md +1 -1
- package/docs/decisions/0002-observer-is-one-self-contained-file.md +1 -1
- package/package.json +1 -1
|
@@ -37,14 +37,18 @@ function fanoutPlanEvents(args) {
|
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
39
39
|
* The run's summary in plain sentences: who took part, what happened to them with the denominator
|
|
40
|
-
* attached, and that the gaps list the participants who did not pass.
|
|
40
|
+
* attached, and that the gaps list the participants who did not pass. A participant the run
|
|
41
|
+
* skipped before its session started never used the app, so it is counted in a sentence of its
|
|
42
|
+
* own.
|
|
41
43
|
*/
|
|
42
44
|
function fanoutSummary(args, facts) {
|
|
43
45
|
const count = args.specs.length;
|
|
44
46
|
const people = plural(count, "participant");
|
|
45
47
|
const rerun = args.rerun === undefined ? "" : `Rerun from ${args.rerun.sourceRunId}: `;
|
|
48
|
+
// The live summary is written once, before any participant starts, so it names the plan: by
|
|
49
|
+
// the time it is read a participant may still be waiting, or skipped.
|
|
46
50
|
if (args.inProgress === true) {
|
|
47
|
-
return `${people} ${count === 1 ? "is" : "are
|
|
51
|
+
return `${people} ${count === 1 ? "is planned, in its own copy" : "are planned, each in their own copy"} of the app. The run is still going, so nothing here is final.`;
|
|
48
52
|
}
|
|
49
53
|
if (args.dryRun) {
|
|
50
54
|
return `${rerun}Dry run: ${people} would each use their own copy of ${subjectName(publicSafeAppUrlLabel(args.appUrl), args.aggregateSubject)}. No desktops were launched and $0 was spent.`;
|
|
@@ -52,7 +56,20 @@ function fanoutSummary(args, facts) {
|
|
|
52
56
|
const tasks = facts.tasks === undefined ? "" : ` Tasks: ${facts.tasks}.`;
|
|
53
57
|
// A host suspension comes first: it may explain everything after it.
|
|
54
58
|
const host = args.hostSuspension === undefined ? "" : `${args.hostSuspension.summary} `;
|
|
55
|
-
|
|
59
|
+
const tookPart = count - facts.skipped;
|
|
60
|
+
const outcomes = facts.outcomes ?? `0/${tookPart} finished a session`;
|
|
61
|
+
const gaps = gapsListClause(tookPart - facts.passed, tookPart);
|
|
62
|
+
const sentences = [];
|
|
63
|
+
// A run where every participant was skipped has no one to describe in this sentence.
|
|
64
|
+
if (tookPart > 0)
|
|
65
|
+
sentences.push(`${plural(tookPart, "participant")} took part, ${tookPart === 1 ? "in its own copy" : "each in their own copy"} of the app: ${outcomes}${gaps}.`);
|
|
66
|
+
// The gaps are pointed at once: here only when the sentence before did not.
|
|
67
|
+
const why = gaps === "" ? "; the gaps say why" : "";
|
|
68
|
+
if (facts.skipped === 1)
|
|
69
|
+
sentences.push(`1 participant was skipped before its session started${why}.`);
|
|
70
|
+
if (facts.skipped > 1)
|
|
71
|
+
sentences.push(`${facts.skipped} participants were skipped before their sessions started${why}.`);
|
|
72
|
+
return `${host}${rerun}${sentences.join(" ")}${tasks}`;
|
|
56
73
|
}
|
|
57
74
|
/**
|
|
58
75
|
* The review's gaps: one line for the participants a host suspension likely failed, then a line
|
|
@@ -75,6 +92,7 @@ function fanoutReview(args, streams, judgment) {
|
|
|
75
92
|
// The judge's verdict: live fan-out must prove every participant (judgeParticipants).
|
|
76
93
|
const verdict = args.verdict;
|
|
77
94
|
const passedParticipants = judgment.participants.filter((participant) => participant.judgedStatus === "passed").length;
|
|
95
|
+
const skippedParticipants = (outcomes ?? []).filter((outcome) => outcome?.skippedReason !== undefined).length;
|
|
78
96
|
// What happened to the participants, with the denominator attached. The verdict above has to
|
|
79
97
|
// collapse the run to one word; this does not (docs/principles/three-roles.md).
|
|
80
98
|
const terminalOutcomes = (outcomes ?? []).filter((outcome) => outcome?.session?.status !== undefined);
|
|
@@ -103,9 +121,10 @@ function fanoutReview(args, streams, judgment) {
|
|
|
103
121
|
// sandbox, so the summary carries its digest, which verify accepts in a shared run.
|
|
104
122
|
summary: fanoutSummary(args, {
|
|
105
123
|
passed: passedParticipants,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
124
|
+
skipped: skippedParticipants,
|
|
125
|
+
...(participants === undefined
|
|
126
|
+
? {}
|
|
127
|
+
: { outcomes: formatParticipantOutcomes(participants, participantEndings) }),
|
|
109
128
|
...(runTasks === undefined ? {} : { tasks: formatRunTaskFunnel(runTasks) }),
|
|
110
129
|
}),
|
|
111
130
|
gaps: args.inProgress === true
|
|
@@ -153,7 +153,7 @@ export function participantSessionOptions(spec, deps, ready, provider, writeScre
|
|
|
153
153
|
const crossed = deps.runBudget.crossed();
|
|
154
154
|
return crossed === undefined
|
|
155
155
|
? null
|
|
156
|
-
: `study budget reached: ${crossed};
|
|
156
|
+
: `study budget reached: ${crossed}; the study's participants stop before their next model request`;
|
|
157
157
|
},
|
|
158
158
|
}),
|
|
159
159
|
...(deps.onObservedUrl === undefined ? {} : { onObservedUrl: deps.onObservedUrl }),
|
package/dist/run/bundle.d.ts
CHANGED
|
@@ -7,14 +7,17 @@ import type { RunInterruptSignal } from "./status.js";
|
|
|
7
7
|
import type { RunSimulationStatus, RunStream, RunStreamKind } from "./streams.js";
|
|
8
8
|
/**
|
|
9
9
|
* The run bundle version this release writes: each stream's trace, and each running participant's
|
|
10
|
-
* items so far, are files run.json indexes
|
|
10
|
+
* items so far, are files run.json indexes, and each participant's per-request lists are only in
|
|
11
|
+
* its trace file (REQUEST_LISTS in src/run/stream-trace.ts).
|
|
11
12
|
*/
|
|
12
|
-
export declare const RUN_BUNDLE_SCHEMA = "humanish.run-bundle.
|
|
13
|
+
export declare const RUN_BUNDLE_SCHEMA = "humanish.run-bundle.v4";
|
|
14
|
+
/** The version 0.125.0 and 0.126.0 wrote, with the per-request lists in run.json too. It is still read. */
|
|
15
|
+
export declare const RUN_BUNDLE_V3_SCHEMA = "humanish.run-bundle.v3";
|
|
13
16
|
/** The version 0.123.0 and 0.124.0 wrote, with a running participant's items inline. It is still read. */
|
|
14
17
|
export declare const RUN_BUNDLE_V2_SCHEMA = "humanish.run-bundle.v2";
|
|
15
18
|
/** The version 0.122.0 and earlier wrote, with every trace inline. It is still read. */
|
|
16
19
|
export declare const RUN_BUNDLE_V1_SCHEMA = "humanish.run-bundle.v1";
|
|
17
|
-
/** Whether `value` names a run bundle version this release reads: v3, v2 or v1. */
|
|
20
|
+
/** Whether `value` names a run bundle version this release reads: v4, v3, v2 or v1. */
|
|
18
21
|
export declare function isReadRunBundleSchema(value: unknown): value is RunBundle["schema"];
|
|
19
22
|
/** The run bundle file, relative to the run directory. */
|
|
20
23
|
export declare const RUN_BUNDLE_FILE = "run.json";
|
|
@@ -238,7 +241,7 @@ export interface RunBundle {
|
|
|
238
241
|
restrictions: ["real-communications"];
|
|
239
242
|
};
|
|
240
243
|
commsReceiving?: CommsReceivingEvidence;
|
|
241
|
-
schema: typeof RUN_BUNDLE_SCHEMA | typeof RUN_BUNDLE_V2_SCHEMA | typeof RUN_BUNDLE_V1_SCHEMA;
|
|
244
|
+
schema: typeof RUN_BUNDLE_SCHEMA | typeof RUN_BUNDLE_V3_SCHEMA | typeof RUN_BUNDLE_V2_SCHEMA | typeof RUN_BUNDLE_V1_SCHEMA;
|
|
242
245
|
runId: string;
|
|
243
246
|
mode: "dry-run" | "live";
|
|
244
247
|
simCount: number;
|
package/dist/run/bundle.js
CHANGED
|
@@ -3,16 +3,22 @@ import { captureGitState } from "./git-state.js";
|
|
|
3
3
|
import { studyFields } from "./study-provenance.js";
|
|
4
4
|
/**
|
|
5
5
|
* The run bundle version this release writes: each stream's trace, and each running participant's
|
|
6
|
-
* items so far, are files run.json indexes
|
|
6
|
+
* items so far, are files run.json indexes, and each participant's per-request lists are only in
|
|
7
|
+
* its trace file (REQUEST_LISTS in src/run/stream-trace.ts).
|
|
7
8
|
*/
|
|
8
|
-
export const RUN_BUNDLE_SCHEMA = "humanish.run-bundle.
|
|
9
|
+
export const RUN_BUNDLE_SCHEMA = "humanish.run-bundle.v4";
|
|
10
|
+
/** The version 0.125.0 and 0.126.0 wrote, with the per-request lists in run.json too. It is still read. */
|
|
11
|
+
export const RUN_BUNDLE_V3_SCHEMA = "humanish.run-bundle.v3";
|
|
9
12
|
/** The version 0.123.0 and 0.124.0 wrote, with a running participant's items inline. It is still read. */
|
|
10
13
|
export const RUN_BUNDLE_V2_SCHEMA = "humanish.run-bundle.v2";
|
|
11
14
|
/** The version 0.122.0 and earlier wrote, with every trace inline. It is still read. */
|
|
12
15
|
export const RUN_BUNDLE_V1_SCHEMA = "humanish.run-bundle.v1";
|
|
13
|
-
/** Whether `value` names a run bundle version this release reads: v3, v2 or v1. */
|
|
16
|
+
/** Whether `value` names a run bundle version this release reads: v4, v3, v2 or v1. */
|
|
14
17
|
export function isReadRunBundleSchema(value) {
|
|
15
|
-
return (value === RUN_BUNDLE_SCHEMA ||
|
|
18
|
+
return (value === RUN_BUNDLE_SCHEMA ||
|
|
19
|
+
value === RUN_BUNDLE_V3_SCHEMA ||
|
|
20
|
+
value === RUN_BUNDLE_V2_SCHEMA ||
|
|
21
|
+
value === RUN_BUNDLE_V1_SCHEMA);
|
|
16
22
|
}
|
|
17
23
|
/** The run bundle file, relative to the run directory. */
|
|
18
24
|
export const RUN_BUNDLE_FILE = "run.json";
|
package/dist/run/detail.js
CHANGED
|
@@ -72,7 +72,7 @@ function participantFrom(stream, index, savedPersonaId) {
|
|
|
72
72
|
...(typeof trace.counts?.turns === "number" ? { turns: trace.counts.turns } : {}),
|
|
73
73
|
...(typeof trace.counts?.actions === "number" ? { actions: trace.counts.actions } : {}),
|
|
74
74
|
...(thoughts > 0 ? { thoughts } : {}),
|
|
75
|
-
...costOf(trace),
|
|
75
|
+
...costOf(trace, stream.traceUnavailable === undefined),
|
|
76
76
|
...(stream.traceUnavailable === undefined
|
|
77
77
|
? {}
|
|
78
78
|
: { evidenceUnavailable: stream.traceUnavailable }),
|
|
@@ -85,13 +85,19 @@ function participantFrom(stream, index, savedPersonaId) {
|
|
|
85
85
|
* model it prices at, so the same estimator gives a figure mid-run instead of the screen reporting
|
|
86
86
|
* the cost as unknown until the moment the run ends. That is the half of a run where knowing what
|
|
87
87
|
* it is costing actually changes what you do.
|
|
88
|
+
*
|
|
89
|
+
* Without `rowsLoaded` (the loader refused the participant's trace) only a recorded figure is
|
|
90
|
+
* shown: long-context pricing needs each request's input size, and the totals alone can price
|
|
91
|
+
* under half of what the rows give.
|
|
88
92
|
*/
|
|
89
|
-
function costOf(trace) {
|
|
93
|
+
function costOf(trace, rowsLoaded) {
|
|
90
94
|
if (trace.executionProfile?.billing === "account-unknown")
|
|
91
95
|
return { estimatedCostUsd: null };
|
|
92
96
|
const recorded = trace.estimatedCost?.estimatedCostUsd;
|
|
93
97
|
if (recorded !== undefined)
|
|
94
98
|
return { estimatedCostUsd: recorded };
|
|
99
|
+
if (!rowsLoaded)
|
|
100
|
+
return {};
|
|
95
101
|
const usage = trace.tokenUsage;
|
|
96
102
|
const model = trace.ids?.model;
|
|
97
103
|
if (usage === undefined || typeof model !== "string")
|
package/dist/run/locate.d.ts
CHANGED
|
@@ -44,8 +44,8 @@ export declare function readSafeRunArtifactJson(runPaths: PreparedRunArtifactPat
|
|
|
44
44
|
* A run's bundle as `humanish verify` reads it, for a reader outside humanish: run.json of run
|
|
45
45
|
* `runInput` (a run id, or `latest`) under `cwdInput`'s `.humanish/runs/`, with every participant's
|
|
46
46
|
* trace file and, while the run is in progress, live file joined, so each stream has its
|
|
47
|
-
* `actor.items`, its screenshot entries and its `liveActor.items` as a v1
|
|
48
|
-
* reads run bundles v1, v2 and
|
|
47
|
+
* `actor.items`, its per-request lists, its screenshot entries and its `liveActor.items` as a v1
|
|
48
|
+
* run.json held them. It reads run bundles v1, v2, v3 and v4. Each file must match the index run.json records for it (path,
|
|
49
49
|
* size, sha256, item count); one that does not throws an Error named `StreamTraceRefusedError`
|
|
50
50
|
* that names the participant. Resolves to null when there is no such run, or its run.json is not
|
|
51
51
|
* a run bundle. `bundlePath` is run.json relative to `cwdInput`, `runDir` the run folder.
|
package/dist/run/locate.js
CHANGED
|
@@ -105,8 +105,8 @@ export async function readSafeRunArtifactJson(runPaths, relativePath) {
|
|
|
105
105
|
* A run's bundle as `humanish verify` reads it, for a reader outside humanish: run.json of run
|
|
106
106
|
* `runInput` (a run id, or `latest`) under `cwdInput`'s `.humanish/runs/`, with every participant's
|
|
107
107
|
* trace file and, while the run is in progress, live file joined, so each stream has its
|
|
108
|
-
* `actor.items`, its screenshot entries and its `liveActor.items` as a v1
|
|
109
|
-
* reads run bundles v1, v2 and
|
|
108
|
+
* `actor.items`, its per-request lists, its screenshot entries and its `liveActor.items` as a v1
|
|
109
|
+
* run.json held them. It reads run bundles v1, v2, v3 and v4. Each file must match the index run.json records for it (path,
|
|
110
110
|
* size, sha256, item count); one that does not throws an Error named `StreamTraceRefusedError`
|
|
111
111
|
* that names the participant. Resolves to null when there is no such run, or its run.json is not
|
|
112
112
|
* a run bundle. `bundlePath` is run.json relative to `cwdInput`, `runDir` the run folder.
|
package/dist/run/run.js
CHANGED
|
@@ -122,8 +122,9 @@ function observerTarget(options) {
|
|
|
122
122
|
* run.json for `bundle`, written after every live file and trace file it indexes, then the removal
|
|
123
123
|
* of the live files it no longer names. Returns the bundle the review and the projections read:
|
|
124
124
|
* raw sandbox ids scrubbed, each finished trace inline, each running participant's items as the
|
|
125
|
-
* index of its live file
|
|
126
|
-
* as they are appended, so the scrub of the bundle no
|
|
125
|
+
* index of its live file with the estimate run.json records for it, and the live files' warning
|
|
126
|
+
* among the events. Live items are scrubbed as they are appended, so the scrub of the bundle no
|
|
127
|
+
* longer walks them.
|
|
127
128
|
*/
|
|
128
129
|
async function writeRunJson(paths, liveItems, bundle) {
|
|
129
130
|
const sandboxIds = await readRunSandboxIds(paths);
|
|
@@ -138,7 +139,14 @@ async function writeRunJson(paths, liveItems, bundle) {
|
|
|
138
139
|
await writeContainedOutputFile(paths, file.path, file.bytes);
|
|
139
140
|
await writeContainedOutputFile(paths, RUN_BUNDLE_FILE, json(split.bundle), "utf8");
|
|
140
141
|
await live.published();
|
|
141
|
-
|
|
142
|
+
// The split prices each running participant from the rows it leaves out of run.json.
|
|
143
|
+
const streams = publicBundle.streams.map((stream, index) => {
|
|
144
|
+
const estimate = split.bundle.streams[index]?.liveActor?.estimatedCost;
|
|
145
|
+
return stream.liveActor === undefined || estimate === undefined
|
|
146
|
+
? stream
|
|
147
|
+
: { ...stream, liveActor: { ...stream.liveActor, estimatedCost: estimate } };
|
|
148
|
+
});
|
|
149
|
+
return { ...publicBundle, streams };
|
|
142
150
|
}
|
|
143
151
|
function runPublisher(args) {
|
|
144
152
|
const { options, runId, createdAt, paths, runStatus, now, admit, participantsRan } = args;
|
|
@@ -7,12 +7,15 @@ export declare class StreamTraceRefusedError extends Error {
|
|
|
7
7
|
readonly streamId: string;
|
|
8
8
|
constructor(streamId: string, detail: string);
|
|
9
9
|
}
|
|
10
|
+
/** `trace` without the per-request lists; every other field as it is. */
|
|
11
|
+
export declare function withoutRequestLists<T extends object>(trace: T): T;
|
|
10
12
|
/**
|
|
11
13
|
* The run.json the run writes, and the trace files it writes first. Each stream with a finished
|
|
12
|
-
* actor keeps that actor without `items
|
|
13
|
-
* trace file; the file is the actor trace with the stream's
|
|
14
|
-
* stream already split (its actor read back from
|
|
15
|
-
* since its file is on disk
|
|
14
|
+
* actor keeps that actor without `items` and the per-request lists, its artifacts other than
|
|
15
|
+
* screenshots, and an index of its trace file; the file is the whole actor trace with the stream's
|
|
16
|
+
* screenshot entries as `artifacts`. A stream already split (its actor read back from run.json
|
|
17
|
+
* without its items) keeps its index, since its file is on disk, and loses any lists an older
|
|
18
|
+
* run.json held beside it. A running participant's live actor is written by liveSummary.
|
|
16
19
|
*/
|
|
17
20
|
export declare function splitStreamTraces(bundle: RunBundle): {
|
|
18
21
|
bundle: RunBundle;
|
|
@@ -54,6 +57,12 @@ type LiveAfter = (index: LiveItemsIndex) => LiveItemsCursor | undefined;
|
|
|
54
57
|
* actor, two participants indexing one file) are loadStreamTraces'.
|
|
55
58
|
*/
|
|
56
59
|
export declare function loadStreamTrace(runPaths: PreparedOutputRoot, stream: RunStream, maxBytes?: number): Promise<RunStream>;
|
|
60
|
+
/**
|
|
61
|
+
* Whether a run.json of `schema` holds the per-request lists (REQUEST_LISTS) the writer kept in
|
|
62
|
+
* it: v1 to v3, and a schema WRITER_RULES does not name, which is read as v1. From v4 they are
|
|
63
|
+
* only in the trace file, so a running participant's `liveActor` holds none.
|
|
64
|
+
*/
|
|
65
|
+
export declare const keepsRequestListsInRunJson: (schema: string) => boolean;
|
|
57
66
|
interface LoadedTraces {
|
|
58
67
|
bundle: RunBundle;
|
|
59
68
|
refused: StreamTraceRefusedError[];
|
|
@@ -85,9 +94,10 @@ export declare function loadStreamTraces(source: PreparedOutputRoot | TraceFileR
|
|
|
85
94
|
}): Promise<LoadedTraces>;
|
|
86
95
|
/**
|
|
87
96
|
* A loaded bundle as a reader that shows a run presents it. A refused stream keeps run.json's
|
|
88
|
-
* summary, shows none of the items run.json or a file supplied for it (its
|
|
89
|
-
* in-progress items, its screenshot entries), and carries
|
|
90
|
-
* warn event on its timeline says the same.
|
|
97
|
+
* summary, shows none of the items or per-request lists run.json or a file supplied for it (its
|
|
98
|
+
* actor's items and lists, its in-progress items, its screenshot entries), and carries
|
|
99
|
+
* `traceUnavailable` with the reason; a warn event on its timeline says the same. A reader that
|
|
100
|
+
* shows the run then never prices or checks rows the loader refused.
|
|
91
101
|
*/
|
|
92
102
|
export declare function displayedTraces(loaded: {
|
|
93
103
|
bundle: RunBundle;
|
package/dist/run/stream-trace.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
// Where a participant's trace lives. A v1 run.json holds every stream's trace items and screenshot
|
|
2
2
|
// entries inline. From v2 the run writer moves them into one file per stream and leaves an index
|
|
3
3
|
// in run.json, so run.json stays a summary whatever the length of the sessions. From v3 a running
|
|
4
|
-
// participant's items so far are in a live file run.json indexes too (src/run/live-items.ts).
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// participant's items so far are in a live file run.json indexes too (src/run/live-items.ts). From
|
|
5
|
+
// v4 the per-request lists (REQUEST_LISTS) are only in the trace file, and a running participant
|
|
6
|
+
// carries its totals and the estimate priced from its rows. Every reader that needs the items or
|
|
7
|
+
// the lists loads them here, which accepts every version and checks each file against its index.
|
|
7
8
|
import { createHash } from "node:crypto";
|
|
8
|
-
import { RUN_BUNDLE_FILE, RUN_BUNDLE_SCHEMA, RUN_BUNDLE_V2_SCHEMA, } from "./bundle.js";
|
|
9
|
+
import { RUN_BUNDLE_FILE, RUN_BUNDLE_SCHEMA, RUN_BUNDLE_V2_SCHEMA, RUN_BUNDLE_V3_SCHEMA, } from "./bundle.js";
|
|
9
10
|
import { isRunBundle } from "./bundle-shape.js";
|
|
10
11
|
import { readContainedPrefix, readContainedRegularFile, refusalText, RUN_ARTIFACT_MAX_BYTES, } from "./contained-output.js";
|
|
11
12
|
import { isLiveItemsIndex, LIVE_ACTOR_SCHEMA, LIVE_ACTOR_V1_SCHEMA, liveItemsAfter, liveItemsFromBytes, } from "./live-items.js";
|
|
13
|
+
import { estimateActorCostForExecution } from "./pricing.js";
|
|
12
14
|
import { isRunStreamArtifact, isStreamTraceIndex, isStreamTracePath } from "./stream-shape.js";
|
|
13
15
|
import { isRecord } from "./type-guards.js";
|
|
14
16
|
/** A trace that does not match its index, or cannot be read. The message names the stream. */
|
|
@@ -43,12 +45,64 @@ function tracePath(stream, index, claimed) {
|
|
|
43
45
|
!POSITION_TRACE_FILE.test(artifact.path))?.path;
|
|
44
46
|
return own !== undefined && !claimed.has(traceFileKey(own)) ? own : `actors/${index + 1}.json`;
|
|
45
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* A participant's lists that gain one entry per model request, as paths into its actor trace. From
|
|
50
|
+
* run bundle v4 run.json leaves them out of `actor` and `liveActor`, and the trace file alone holds
|
|
51
|
+
* them; the Observer's data leaves them out too.
|
|
52
|
+
*/
|
|
53
|
+
const REQUEST_LISTS = ["tokenUsage.turns", "conversation.requests", "providerRequests"];
|
|
54
|
+
/** The value at `list`'s path in `trace`, when the object holding it is there. */
|
|
55
|
+
function listIn(trace, list) {
|
|
56
|
+
const [first, second] = list.split(".");
|
|
57
|
+
if (second === undefined)
|
|
58
|
+
return trace[first];
|
|
59
|
+
const parent = trace[first];
|
|
60
|
+
return isRecord(parent) ? parent[second] : undefined;
|
|
61
|
+
}
|
|
62
|
+
/** `trace` without the per-request lists; every other field as it is. */
|
|
63
|
+
export function withoutRequestLists(trace) {
|
|
64
|
+
const copy = { ...trace };
|
|
65
|
+
for (const list of REQUEST_LISTS) {
|
|
66
|
+
const [first, second] = list.split(".");
|
|
67
|
+
const parent = copy[first];
|
|
68
|
+
if (second === undefined)
|
|
69
|
+
delete copy[first];
|
|
70
|
+
else if (isRecord(parent) && second in parent) {
|
|
71
|
+
const { [second]: _list, ...rest } = parent;
|
|
72
|
+
copy[first] = rest;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return copy;
|
|
76
|
+
}
|
|
77
|
+
/** The first per-request list `trace` holds, if any. */
|
|
78
|
+
function heldRequestList(trace) {
|
|
79
|
+
return REQUEST_LISTS.find((list) => listIn(trace, list) !== undefined);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* run.json's live actor: without the per-request lists, and with the estimate priced from the
|
|
83
|
+
* rows it leaves out. Long-context pricing needs each request's input size, so a figure priced
|
|
84
|
+
* from the totals alone could be under half the real one. A live actor read back from run.json
|
|
85
|
+
* has no rows and keeps the estimate they gave.
|
|
86
|
+
*/
|
|
87
|
+
function liveSummary(live) {
|
|
88
|
+
const summary = withoutRequestLists(live);
|
|
89
|
+
const model = live.ids?.model;
|
|
90
|
+
if (live.tokenUsage === undefined || typeof model !== "string")
|
|
91
|
+
return summary;
|
|
92
|
+
if (live.tokenUsage.turns === undefined && live.estimatedCost !== undefined)
|
|
93
|
+
return summary;
|
|
94
|
+
return {
|
|
95
|
+
...summary,
|
|
96
|
+
estimatedCost: estimateActorCostForExecution(live.tokenUsage, model, live.executionProfile),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
46
99
|
/**
|
|
47
100
|
* The run.json the run writes, and the trace files it writes first. Each stream with a finished
|
|
48
|
-
* actor keeps that actor without `items
|
|
49
|
-
* trace file; the file is the actor trace with the stream's
|
|
50
|
-
* stream already split (its actor read back from
|
|
51
|
-
* since its file is on disk
|
|
101
|
+
* actor keeps that actor without `items` and the per-request lists, its artifacts other than
|
|
102
|
+
* screenshots, and an index of its trace file; the file is the whole actor trace with the stream's
|
|
103
|
+
* screenshot entries as `artifacts`. A stream already split (its actor read back from run.json
|
|
104
|
+
* without its items) keeps its index, since its file is on disk, and loses any lists an older
|
|
105
|
+
* run.json held beside it. A running participant's live actor is written by liveSummary.
|
|
52
106
|
*/
|
|
53
107
|
export function splitStreamTraces(bundle) {
|
|
54
108
|
const files = [];
|
|
@@ -56,10 +110,15 @@ export function splitStreamTraces(bundle) {
|
|
|
56
110
|
for (const stream of bundle.streams)
|
|
57
111
|
if (stream.trace)
|
|
58
112
|
claimed.add(traceFileKey(stream.trace.path));
|
|
59
|
-
const streams = bundle.streams.map((
|
|
113
|
+
const streams = bundle.streams.map((written, index) => {
|
|
114
|
+
const stream = written.liveActor === undefined
|
|
115
|
+
? written
|
|
116
|
+
: { ...written, liveActor: liveSummary(written.liveActor) };
|
|
60
117
|
const actor = stream.actor;
|
|
61
|
-
if (actor === undefined
|
|
118
|
+
if (actor === undefined)
|
|
62
119
|
return stream;
|
|
120
|
+
if (!Array.isArray(actor.items))
|
|
121
|
+
return { ...stream, actor: withoutRequestLists(actor) };
|
|
63
122
|
const { trace: _previous, ...rest } = stream;
|
|
64
123
|
const filePath = tracePath(rest, index, claimed);
|
|
65
124
|
// Only a stream already split that moved position can hold this stream's position name.
|
|
@@ -72,8 +131,8 @@ export function splitStreamTraces(bundle) {
|
|
|
72
131
|
const { items, ...summary } = actor;
|
|
73
132
|
return {
|
|
74
133
|
...rest,
|
|
75
|
-
// run.json's actor is the summary: every field of the trace except its items.
|
|
76
|
-
actor: summary,
|
|
134
|
+
// run.json's actor is the summary: every field of the trace except its items and lists.
|
|
135
|
+
actor: withoutRequestLists(summary),
|
|
77
136
|
artifacts: stream.artifacts.filter((artifact) => !isScreenshot(artifact)),
|
|
78
137
|
trace: {
|
|
79
138
|
path: filePath,
|
|
@@ -125,13 +184,38 @@ function joinTrace(stream, bytes) {
|
|
|
125
184
|
refuse(`${index.path} lists an artifact that is not a screenshot entry`);
|
|
126
185
|
if (value.items.length !== index.items || artifacts.length !== index.screenshots)
|
|
127
186
|
refuse(`${index.path} holds ${value.items.length} items and ${artifacts.length} screenshots and run.json records ${index.items} and ${index.screenshots}`);
|
|
187
|
+
const actor = joinRequestLists(stream.actor, value, index.path, refuse);
|
|
128
188
|
const { trace: _index, ...rest } = stream;
|
|
129
189
|
return {
|
|
130
190
|
...rest,
|
|
131
|
-
actor: { ...
|
|
191
|
+
actor: { ...actor, items: value.items },
|
|
132
192
|
artifacts: [...stream.artifacts, ...artifacts],
|
|
133
193
|
};
|
|
134
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* run.json's actor with each per-request list its trace file holds, whatever the bundle's
|
|
197
|
+
* version: before v4 the writer wrote run.json's lists and the file's from one actor, so they are
|
|
198
|
+
* equal. A nested list goes into run.json's `tokenUsage` or `conversation` when run.json has it.
|
|
199
|
+
*/
|
|
200
|
+
function joinRequestLists(summary, file, filePath, refuse) {
|
|
201
|
+
if (isRecord(file.conversation) && !Array.isArray(file.conversation.requests))
|
|
202
|
+
refuse(`${filePath} records a conversation without its requests list`);
|
|
203
|
+
const joined = { ...summary };
|
|
204
|
+
for (const list of REQUEST_LISTS) {
|
|
205
|
+
const value = listIn(file, list);
|
|
206
|
+
if (value === undefined)
|
|
207
|
+
continue;
|
|
208
|
+
if (!Array.isArray(value))
|
|
209
|
+
refuse(`${filePath} holds ${list} as something other than a list`);
|
|
210
|
+
const [first, second] = list.split(".");
|
|
211
|
+
const parent = joined[first];
|
|
212
|
+
if (second === undefined)
|
|
213
|
+
joined[first] = value;
|
|
214
|
+
else if (isRecord(parent))
|
|
215
|
+
joined[first] = { ...parent, [second]: value };
|
|
216
|
+
}
|
|
217
|
+
return joined;
|
|
218
|
+
}
|
|
135
219
|
/** Refuses an index the shape check would refuse, naming a file that is not a trace file name. */
|
|
136
220
|
function assertIndex(stream) {
|
|
137
221
|
const index = stream.trace;
|
|
@@ -242,16 +326,30 @@ async function readStreamTrace(read, stream, maxBytes, liveAfter) {
|
|
|
242
326
|
export async function loadStreamTrace(runPaths, stream, maxBytes = RUN_ARTIFACT_MAX_BYTES) {
|
|
243
327
|
return (await readStreamTrace(containedReader(runPaths), stream, maxBytes)).stream;
|
|
244
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* What each version's writer never puts in run.json, so a run.json of that version that holds it
|
|
331
|
+
* did not come from the writer. From v2 the writer indexes every actor on every write, mid-run
|
|
332
|
+
* flushes included, and the final write replaces `liveActor`. From v3 a running participant's
|
|
333
|
+
* items are in a live file (humanish.live-actor.v2). From v4 the per-request lists are only in
|
|
334
|
+
* the trace file. A run.json of v1, or of no version this table names, is read as v1 is.
|
|
335
|
+
*/
|
|
336
|
+
const WRITER_RULES = new Map([
|
|
337
|
+
[RUN_BUNDLE_V2_SCHEMA, { liveFiles: false, listsOut: false }],
|
|
338
|
+
[RUN_BUNDLE_V3_SCHEMA, { liveFiles: true, listsOut: false }],
|
|
339
|
+
[RUN_BUNDLE_SCHEMA, { liveFiles: true, listsOut: true }],
|
|
340
|
+
]);
|
|
341
|
+
/**
|
|
342
|
+
* Whether a run.json of `schema` holds the per-request lists (REQUEST_LISTS) the writer kept in
|
|
343
|
+
* it: v1 to v3, and a schema WRITER_RULES does not name, which is read as v1. From v4 they are
|
|
344
|
+
* only in the trace file, so a running participant's `liveActor` holds none.
|
|
345
|
+
*/
|
|
346
|
+
export const keepsRequestListsInRunJson = (schema) => WRITER_RULES.get(schema)?.listsOut !== true;
|
|
245
347
|
/**
|
|
246
348
|
* Why a stream of this bundle cannot be read, before any file is. A trace file belongs to one
|
|
247
349
|
* participant, so every stream indexing a path another stream indexes, compared as a
|
|
248
|
-
* case-insensitive file system compares them, is refused.
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
* participants' items so far as `liveActor`: inline in v2 (humanish.live-actor.v1), in a live file
|
|
252
|
-
* from v3 (humanish.live-actor.v2), so a v3 run.json with inline live items did not come from the
|
|
253
|
-
* writer. A run.json whose outcome says the run finished never carries `liveActor`, since the
|
|
254
|
-
* final write replaces it.
|
|
350
|
+
* case-insensitive file system compares them, is refused in every version. The other rules are
|
|
351
|
+
* WRITER_RULES': a v2 actor without an index lost its trace file, and a run.json whose outcome says
|
|
352
|
+
* the run finished never carries `liveActor`.
|
|
255
353
|
*/
|
|
256
354
|
function bundleRefusal(bundle, stream, owners) {
|
|
257
355
|
const shared = isRecord(stream.trace)
|
|
@@ -259,16 +357,23 @@ function bundleRefusal(bundle, stream, owners) {
|
|
|
259
357
|
: undefined;
|
|
260
358
|
if (shared !== undefined && shared.length > 1)
|
|
261
359
|
return `run.json indexes ${String(stream.trace.path)} for participants ${shared.join(", ")} (paths that differ only in case name one file on some file systems), and a trace file belongs to one participant`;
|
|
262
|
-
|
|
360
|
+
const rules = WRITER_RULES.get(String(bundle.schema));
|
|
361
|
+
if (rules === undefined)
|
|
263
362
|
return undefined;
|
|
363
|
+
const version = String(bundle.schema).replace("humanish.run-bundle.", "");
|
|
264
364
|
if (stream.actor !== undefined && stream.trace === undefined)
|
|
265
365
|
return "run.json holds its actor and names no trace file";
|
|
266
366
|
if (stream.liveActor !== undefined && bundle.outcome?.state === "finished")
|
|
267
367
|
return "run.json records a finished run and still holds in-progress items";
|
|
268
|
-
if (
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
368
|
+
if (rules.liveFiles && stream.liveActor !== undefined && isInlineLiveActor(stream.liveActor))
|
|
369
|
+
return `run.json ${version} holds in-progress items inline, which the writer keeps in a live file`;
|
|
370
|
+
if (rules.listsOut)
|
|
371
|
+
for (const field of ["actor", "liveActor"]) {
|
|
372
|
+
const trace = stream[field];
|
|
373
|
+
const list = isRecord(trace) ? heldRequestList(trace) : undefined;
|
|
374
|
+
if (list !== undefined)
|
|
375
|
+
return `run.json ${version} holds ${list} in its ${field}, which the writer keeps only in the trace file`;
|
|
376
|
+
}
|
|
272
377
|
return undefined;
|
|
273
378
|
}
|
|
274
379
|
/**
|
|
@@ -348,9 +453,10 @@ async function loadEveryStream(read, bundle, maxBytes, liveAfter) {
|
|
|
348
453
|
}
|
|
349
454
|
/**
|
|
350
455
|
* A loaded bundle as a reader that shows a run presents it. A refused stream keeps run.json's
|
|
351
|
-
* summary, shows none of the items run.json or a file supplied for it (its
|
|
352
|
-
* in-progress items, its screenshot entries), and carries
|
|
353
|
-
* warn event on its timeline says the same.
|
|
456
|
+
* summary, shows none of the items or per-request lists run.json or a file supplied for it (its
|
|
457
|
+
* actor's items and lists, its in-progress items, its screenshot entries), and carries
|
|
458
|
+
* `traceUnavailable` with the reason; a warn event on its timeline says the same. A reader that
|
|
459
|
+
* shows the run then never prices or checks rows the loader refused.
|
|
354
460
|
*/
|
|
355
461
|
export function displayedTraces(loaded) {
|
|
356
462
|
const reasons = new Map(loaded.refused.map((error) => [error.streamId, error.message]));
|
|
@@ -363,7 +469,7 @@ export function displayedTraces(loaded) {
|
|
|
363
469
|
const { liveActor: _live, trace: _index, actor, ...rest } = stream;
|
|
364
470
|
return {
|
|
365
471
|
...rest,
|
|
366
|
-
...(actor === undefined ? {} : { actor: { ...actor, items: [] } }),
|
|
472
|
+
...(actor === undefined ? {} : { actor: { ...withoutRequestLists(actor), items: [] } }),
|
|
367
473
|
artifacts: stream.artifacts.filter((artifact) => !isScreenshot(artifact)),
|
|
368
474
|
traceUnavailable: reason,
|
|
369
475
|
};
|
package/dist/run/streams.d.ts
CHANGED
|
@@ -289,7 +289,10 @@ export interface RunStream {
|
|
|
289
289
|
* completionReason, or completedAt, and this shape cannot claim them. Present only on
|
|
290
290
|
* `inProgress` bundles; the final write replaces it with the real `actor` and never
|
|
291
291
|
* carries it. A v3 run.json holds `humanish.live-actor.v2` with `itemsFile` in place of `items`;
|
|
292
|
-
* the trace loader (src/run/stream-trace.ts) reads the file and puts the items back.
|
|
292
|
+
* the trace loader (src/run/stream-trace.ts) reads the file and puts the items back. A v4
|
|
293
|
+
* run.json's live actor also leaves out `tokenUsage.turns` and `providerRequests`, which are
|
|
294
|
+
* written only in the trace when the participant finishes, and carries the `estimatedCost` the
|
|
295
|
+
* run writer priced from those rows.
|
|
293
296
|
*/
|
|
294
297
|
liveActor?: {
|
|
295
298
|
schema: "humanish.live-actor.v1" | "humanish.live-actor.v2";
|
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
// RUN_ARTIFACT_MAX_BYTES, and the public-safety scan visits at most runEntryLimit(participants)
|
|
5
5
|
// files and folders.
|
|
6
6
|
//
|
|
7
|
-
// observer/index.html carries every participant's items gzipped, so with busy participants it
|
|
8
|
-
//
|
|
9
|
-
// working 8 hours.
|
|
10
|
-
//
|
|
11
|
-
// 16 MiB,
|
|
7
|
+
// observer/index.html carries every participant's items gzipped, so with busy participants it
|
|
8
|
+
// passes verify's 32 MiB long before run.json passes analysis's 16 MiB: at these rates, 20
|
|
9
|
+
// participants working 8 hours. From run bundle v4 run.json grows by participant, not by model
|
|
10
|
+
// request: each participant's per-request lists are only in its trace. run.json can still pass
|
|
11
|
+
// 16 MiB, since a study's mission is not capped and each participant's stream copies it.
|
|
12
|
+
// observer-data.json leaves the lists out too and was 0.94 to 1.12 of run.json's size on the live
|
|
13
|
+
// runs measured, so it passes 32 MiB only after run.json passes 16 MiB, and the estimate does not
|
|
14
|
+
// check it.
|
|
12
15
|
import { EVIDENCE_LIMITS } from "../analysis/analysis-limits.js";
|
|
13
16
|
import { RUN_ARTIFACT_MAX_BYTES } from "../run/contained-output.js";
|
|
14
17
|
import { formatDuration } from "../run/projection.js";
|
|
@@ -20,19 +23,16 @@ import { runEntryLimit } from "../verify/scan-limit.js";
|
|
|
20
23
|
// own files. Pace, session length and screenshots: the live computer-use runs kept on the
|
|
21
24
|
// maintainer's machine. A trace's rate a turn on the public site/public/runs/lobby-0927 run
|
|
22
25
|
// (8 participants, 20 to 78 turns) was 1,579 bytes plus 91 for the page, under the live fit.
|
|
26
|
+
// The run.json and page data rates were measured again on those runs written as v4.
|
|
23
27
|
const RATES = Object.freeze({
|
|
24
28
|
/** run.json bytes for the run itself: its header, study, review and cost. Live fit: 4,381. */
|
|
25
29
|
runBytes: 4_400,
|
|
26
30
|
/**
|
|
27
|
-
* run.json bytes for each participant
|
|
28
|
-
*
|
|
31
|
+
* run.json bytes for each participant: its stream, actor summary and closing report request.
|
|
32
|
+
* Live fit on v3: 16,239. Written as v4, the kept runs of one participant held 20,571 to 23,160
|
|
33
|
+
* bytes and those of two 30,647 to 38,948, beside 20,900 and 37,400 at these rates.
|
|
29
34
|
*/
|
|
30
35
|
participantBytes: 16_500,
|
|
31
|
-
/**
|
|
32
|
-
* run.json bytes a model turn adds: the request's entries in `tokenUsage.turns` and
|
|
33
|
-
* `conversation.requests`. Live: 30,623 bytes over 137 requests, 223.5 each.
|
|
34
|
-
*/
|
|
35
|
-
turnRunBytes: 225,
|
|
36
36
|
/** A trace file's bytes before its turns: the actor summary. Live fit: 5,378. */
|
|
37
37
|
participantTraceBytes: 5_400,
|
|
38
38
|
/**
|
|
@@ -63,10 +63,10 @@ const RATES = Object.freeze({
|
|
|
63
63
|
/** observer/index.html's app before any run data: 1,199,831 bytes on the dry runs. */
|
|
64
64
|
observerAppBytes: 1_200_000,
|
|
65
65
|
/**
|
|
66
|
-
* The page's compact copy of observer-data.json, as a share of run.json: 0.
|
|
67
|
-
*
|
|
66
|
+
* The page's compact copy of observer-data.json, as a share of run.json: 0.71 to 0.84 on the
|
|
67
|
+
* live runs written as v4, where neither carries the per-request lists.
|
|
68
68
|
*/
|
|
69
|
-
observerPageDataShare: 0.
|
|
69
|
+
observerPageDataShare: 0.85,
|
|
70
70
|
/**
|
|
71
71
|
* The page's gzipped base64 items, as a share of all the traces' bytes: 0.087 to 0.114 on the
|
|
72
72
|
* live runs of two participants, 0.097 for the busiest participant alone (about 475 bytes a
|
|
@@ -124,7 +124,7 @@ export function estimateBundle(participants, sessionMs) {
|
|
|
124
124
|
largestTrace = Math.max(largestTrace, trace);
|
|
125
125
|
}
|
|
126
126
|
const count = participants.length;
|
|
127
|
-
const runFileBytes = RATES.runBytes + count * RATES.participantBytes
|
|
127
|
+
const runFileBytes = RATES.runBytes + count * RATES.participantBytes;
|
|
128
128
|
const screenshots = count + turns + frames;
|
|
129
129
|
const observerPageBytes = Math.round(RATES.observerAppBytes +
|
|
130
130
|
RATES.observerPageDataShare * runFileBytes +
|
|
@@ -204,7 +204,7 @@ export function bundleLimitReason(estimate) {
|
|
|
204
204
|
: `${changes.slice(0, -1).join(", ")}, or ${changes.at(-1)}`;
|
|
205
205
|
return [
|
|
206
206
|
...passed,
|
|
207
|
-
`The estimate counts ${counted.join(" and ")}, at a model turn every ${formatDuration(RATES.secondsPerTurn * 1000)} that adds
|
|
207
|
+
`The estimate counts ${counted.join(" and ")}, at a model turn every ${formatDuration(RATES.secondsPerTurn * 1000)} that adds up to ${(RATES.turnTraceBytes / 1000).toFixed(1)} KB to the participant's trace.`,
|
|
208
208
|
"The run goes ahead.",
|
|
209
209
|
`To stay under ${passed.length === 1 ? "the limit" : "the limits"}, ${advice}.`,
|
|
210
210
|
].join(" ");
|
package/dist/verify/costs.d.ts
CHANGED
|
@@ -24,7 +24,11 @@ export declare function contradictsAccountBilling(streams: readonly {
|
|
|
24
24
|
* a null estimate, or a participant without estimatedCost is fine. A non-null figure must carry its
|
|
25
25
|
* ratesAsOf date + source; a number total must equal round6(sum of only the non-null lines) and a
|
|
26
26
|
* null line may never be coerced to 0. A null estimate must be declared (a reason + null
|
|
27
|
-
* ratesAsOf), mirroring the terminal no-spend proof's null-discipline.
|
|
28
|
-
*
|
|
27
|
+
* ratesAsOf), mirroring the terminal no-spend proof's null-discipline. A running participant's
|
|
28
|
+
* estimate is checked the same way. Account-billed participants also have their execution profile
|
|
29
|
+
* and provider request receipts checked here. From v4 a running participant's receipts are written
|
|
30
|
+
* only in its trace when it finishes, so its live actor holds none. The receipts of a stream in
|
|
31
|
+
* `refused` (its trace the loader refused) are not checked: from v4 they are only in that trace,
|
|
32
|
+
* and verify reports the refusal.
|
|
29
33
|
*/
|
|
30
|
-
export declare function costAndReceiptFindings(bundle: RunBundle): string[];
|
|
34
|
+
export declare function costAndReceiptFindings(bundle: RunBundle, refused: ReadonlySet<string>): string[];
|