humanish 0.117.0 → 0.119.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 +42 -0
- package/dist/analysis/admission.d.ts +23 -4
- package/dist/analysis/admission.js +47 -9
- package/dist/analysis/analysis-limits.d.ts +32 -0
- package/dist/analysis/analysis-limits.js +41 -3
- package/dist/analysis/automatic-config.d.ts +11 -1
- package/dist/analysis/automatic-config.js +26 -11
- package/dist/analysis/cohorts.d.ts +103 -0
- package/dist/analysis/cohorts.js +80 -0
- package/dist/analysis/evidence.d.ts +5 -10
- package/dist/analysis/evidence.js +37 -34
- package/dist/analysis/execute.d.ts +24 -7
- package/dist/analysis/execute.js +290 -307
- package/dist/analysis/responses.d.ts +19 -0
- package/dist/analysis/responses.js +201 -0
- package/dist/analysis/store.js +2 -1
- package/dist/analysis/validation.js +13 -7
- package/dist/cli/commands/analyze.js +10 -4
- package/dist/cli/commands/study-format.js +8 -11
- package/dist/cli/commands/study-route-shared-world.js +3 -1
- package/dist/cli/commands/study-run.js +1 -1
- package/dist/cli/doctor.js +11 -1
- package/dist/comms/lease-store.d.ts +1 -1
- package/dist/comms/lease-store.js +3 -2
- package/dist/comms/receiving-types.d.ts +2 -0
- package/dist/comms/receiving-types.js +2 -0
- package/dist/observer-app.html +2 -2
- package/dist/routes/computer-use/fanout-bundle.js +4 -2
- package/dist/routes/computer-use/fanout-records.js +6 -1
- package/dist/routes/computer-use/participant-execution.d.ts +6 -4
- package/dist/routes/computer-use/participant-execution.js +42 -14
- package/dist/routes/computer-use/participant-model.js +18 -8
- package/dist/routes/computer-use/participant-runs.js +15 -9
- package/dist/routes/computer-use/plan.d.ts +3 -1
- package/dist/routes/computer-use/plan.js +38 -12
- package/dist/routes/computer-use/rerun-selection.js +14 -3
- package/dist/routes/computer-use/single-bundle.js +7 -4
- package/dist/routes/computer-use/subject-projection.js +2 -0
- package/dist/routes/computer-use/types.d.ts +13 -1
- package/dist/routes/scripted/plan.d.ts +4 -1
- package/dist/routes/scripted/plan.js +25 -2
- package/dist/routes/scripted/setup.js +1 -4
- package/dist/routes/scripted/types.d.ts +1 -1
- package/dist/routes/shared-world/bundle-records.js +14 -0
- package/dist/routes/shared-world/bundle.js +2 -1
- package/dist/routes/shared-world/external-public.js +15 -3
- package/dist/routes/shared-world/handoff.d.ts +2 -2
- package/dist/routes/shared-world/participant-specs.js +2 -2
- package/dist/routes/shared-world/plan.d.ts +10 -1
- package/dist/routes/shared-world/plan.js +86 -10
- package/dist/routes/shared-world/provisioned.js +15 -8
- package/dist/routes/shared-world/route.js +7 -4
- package/dist/routes/shared-world/types.d.ts +2 -0
- package/dist/run/bundle-shape.js +12 -2
- package/dist/run/bundle.d.ts +20 -3
- package/dist/run/participant-records.js +1 -0
- package/dist/run/projection.js +7 -2
- package/dist/run/run.js +4 -2
- package/dist/run/subject-name.d.ts +11 -0
- package/dist/run/subject-name.js +11 -0
- package/dist/study/arrivals.d.ts +114 -0
- package/dist/study/arrivals.js +204 -0
- package/dist/study/composition-rules.js +1 -1
- package/dist/study/config.js +6 -20
- package/dist/study/doctor.js +2 -1
- package/dist/study/keys.d.ts +1 -0
- package/dist/study/keys.js +2 -0
- package/dist/study/migrate/v2.js +3 -2
- package/dist/study/parse/actors.d.ts +2 -2
- package/dist/study/parse/actors.js +10 -4
- package/dist/study/parse/front.d.ts +1 -1
- package/dist/study/parse/front.js +36 -2
- package/dist/study/plan-base.d.ts +1 -1
- package/dist/study/plan-participants.d.ts +2 -0
- package/dist/study/plan-participants.js +3 -5
- package/dist/study/plan-types.d.ts +13 -3
- package/dist/study/plan.d.ts +4 -4
- package/dist/study/plan.js +21 -12
- package/dist/study/preflight.d.ts +2 -1
- package/dist/study/preflight.js +24 -3
- package/dist/study/routing.d.ts +5 -4
- package/dist/study/routing.js +16 -7
- package/dist/study/types.d.ts +14 -0
- package/dist/study/validation.d.ts +2 -2
- package/dist/study/validation.js +27 -14
- package/dist/study/warnings.js +11 -3
- package/dist/substrates/e2b/lifetime.d.ts +75 -4
- package/dist/substrates/e2b/lifetime.js +134 -10
- package/dist/substrates/e2b/subject-sandbox.js +2 -6
- package/dist/tui-app.js +112 -112
- package/dist/verify/artifacts.d.ts +19 -4
- package/dist/verify/artifacts.js +39 -8
- package/dist/verify/verify.js +6 -15
- package/docs/contracts/run-bundle.md +8 -7
- package/docs/contracts/schemas.md +33 -9
- package/docs/contracts/study-analysis.md +61 -8
- package/docs/principles/invariants-and-defaults.md +14 -14
- package/docs/product/automatic-analysis.md +10 -3
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@ import type { BrowserSurface } from "../actors/scripted-browser/types.js";
|
|
|
5
5
|
import type { ScriptedRefusal } from "../routes/scripted/plan.js";
|
|
6
6
|
import type { TerminalRefusal } from "../routes/terminal/plan.js";
|
|
7
7
|
import type { RunStudyProvenance } from "../run/study-provenance.js";
|
|
8
|
+
import type { ArrivalPlan } from "./arrivals.js";
|
|
8
9
|
import type { ComputerUseRefusal } from "../routes/computer-use/plan.js";
|
|
9
10
|
import type { SharedWorldRefusal } from "../routes/shared-world/plan.js";
|
|
10
11
|
import type { ComputerUseParticipant, ExternalPublicParticipant, ProvisionedParticipant } from "./plan-participants.js";
|
|
@@ -63,7 +64,8 @@ interface PlanBase {
|
|
|
63
64
|
/** The study's declared title, which bundles record. */
|
|
64
65
|
readonly title?: string;
|
|
65
66
|
readonly study?: RunStudyProvenance;
|
|
66
|
-
/** Warnings about the study's own fields
|
|
67
|
+
/** Warnings about the study's own fields and the plan made from them, which the run records in
|
|
68
|
+
* its bundle as warn events. */
|
|
67
69
|
readonly warnings?: readonly string[];
|
|
68
70
|
/** A frozen copy owned by the plan. */
|
|
69
71
|
readonly residual: Readonly<ResidualConfig>;
|
|
@@ -162,6 +164,8 @@ export interface ComputerUsePlan extends PlanBase {
|
|
|
162
164
|
readonly runner: ComputerUseRunner;
|
|
163
165
|
/** Declared cap clamped to [1, participants]; the route may only lower it from env. */
|
|
164
166
|
readonly concurrency: number;
|
|
167
|
+
/** When the participants start and how many run at once, with every session at its budget. */
|
|
168
|
+
readonly arrivals: ArrivalPlan;
|
|
165
169
|
readonly sessionBudgetMs: number;
|
|
166
170
|
readonly sandboxMs: number;
|
|
167
171
|
readonly caps: {
|
|
@@ -196,6 +200,11 @@ export interface SharedWorldPlan extends PlanBase {
|
|
|
196
200
|
readonly plane: SharedWorldPlane;
|
|
197
201
|
/** At least 2. */
|
|
198
202
|
readonly concurrency: number;
|
|
203
|
+
/**
|
|
204
|
+
* When the participants start and how many run at once, with every session at its budget. On
|
|
205
|
+
* the provisioned plane the subject sandbox serves until `lastEndMs`.
|
|
206
|
+
*/
|
|
207
|
+
readonly arrivals: ArrivalPlan;
|
|
199
208
|
/** Each participant's session budget: execution.timeoutMs, else the plane's derived default. */
|
|
200
209
|
readonly sessionTimeoutMs: number;
|
|
201
210
|
readonly brain: Extract<Brain, {
|
|
@@ -258,9 +267,10 @@ export interface ScriptedPlan extends PlanBase {
|
|
|
258
267
|
readonly scenarioRef: string;
|
|
259
268
|
/** Empty only for a library caller's `count: 0`, which the parser refuses; that run has no sessions. */
|
|
260
269
|
readonly surfaces: readonly BrowserSurface[];
|
|
261
|
-
/** The declared persona
|
|
270
|
+
/** The declared persona; the route supplies its default. */
|
|
262
271
|
readonly personaId?: string;
|
|
263
|
-
|
|
272
|
+
/** Each surface's journey budget: execution.timeoutMs, else 5 minutes. */
|
|
273
|
+
readonly sessionTimeoutMs: number;
|
|
264
274
|
}
|
|
265
275
|
export type StudyPlan = PreviewPlan | ComputerUsePlan | SharedWorldPlan | TerminalPlan | ScriptedPlan;
|
|
266
276
|
interface PlannedStudy {
|
package/dist/study/plan.d.ts
CHANGED
|
@@ -7,9 +7,9 @@ export { routeOf, type StudyRoute } from "./routing.js";
|
|
|
7
7
|
export declare function resolveStudyDryRun(config: StudyConfig, override: boolean | undefined, fallback: boolean | undefined): boolean | undefined;
|
|
8
8
|
/**
|
|
9
9
|
* The plan a study runs under, built without reading files or the network. From the environment
|
|
10
|
-
* (options.env, else process.env, as the routes read it) it reads
|
|
11
|
-
* ceiling, which no study file can know. Each route's
|
|
12
|
-
* makes, in the route's order and with its codes and
|
|
13
|
-
* the same planner.
|
|
10
|
+
* (options.env, else process.env, as the routes read it) it reads the operator's E2B plan limits,
|
|
11
|
+
* the sandbox ceiling and the concurrent sandboxes, which no study file can know. Each route's
|
|
12
|
+
* planner makes every refusal that route makes, in the route's order and with its codes and
|
|
13
|
+
* messages; the route's exported runner calls the same planner.
|
|
14
14
|
*/
|
|
15
15
|
export declare function planStudy(config: StudyConfig, options: InternalRunStudyOptions, deps?: StudyDeps): PlanResult;
|
package/dist/study/plan.js
CHANGED
|
@@ -6,7 +6,7 @@ import { callerDrivingOf, planComputerUseStudy } from "../routes/computer-use/pl
|
|
|
6
6
|
import { injectedBrowser, planScriptedStudy } from "../routes/scripted/plan.js";
|
|
7
7
|
import { planSharedWorldStudy } from "../routes/shared-world/plan.js";
|
|
8
8
|
import { planTerminalStudy } from "../routes/terminal/plan.js";
|
|
9
|
-
import { sandboxCeiling } from "../substrates/e2b/lifetime.js";
|
|
9
|
+
import { concurrentSandboxes, sandboxCeiling, } from "../substrates/e2b/lifetime.js";
|
|
10
10
|
import { localBrowserDefaults } from "../substrates/local/runtime-config.js";
|
|
11
11
|
import { THIS_REPO_DRY_RUN_ONLY } from "./composition-rules.js";
|
|
12
12
|
import { planBase } from "./plan-base.js";
|
|
@@ -65,21 +65,25 @@ function planPreview(config, options, input) {
|
|
|
65
65
|
}
|
|
66
66
|
/**
|
|
67
67
|
* The plan a study runs under, built without reading files or the network. From the environment
|
|
68
|
-
* (options.env, else process.env, as the routes read it) it reads
|
|
69
|
-
* ceiling, which no study file can know. Each route's
|
|
70
|
-
* makes, in the route's order and with its codes and
|
|
71
|
-
* the same planner.
|
|
68
|
+
* (options.env, else process.env, as the routes read it) it reads the operator's E2B plan limits,
|
|
69
|
+
* the sandbox ceiling and the concurrent sandboxes, which no study file can know. Each route's
|
|
70
|
+
* planner makes every refusal that route makes, in the route's order and with its codes and
|
|
71
|
+
* messages; the route's exported runner calls the same planner.
|
|
72
72
|
*/
|
|
73
73
|
export function planStudy(config, options, deps = {}) {
|
|
74
74
|
const study = localBrowserDefaults(config);
|
|
75
75
|
const input = { dryRun: resolveStudyDryRun(study, options.dryRun, true) ?? true };
|
|
76
|
-
const
|
|
77
|
-
const
|
|
76
|
+
const env = options.env ?? process.env;
|
|
77
|
+
const e2b = {
|
|
78
|
+
sandboxCeiling: sandboxCeiling(env),
|
|
79
|
+
concurrentSandboxes: concurrentSandboxes(env),
|
|
80
|
+
};
|
|
81
|
+
const result = planRoute(routeOf(config), study, options, input, deps, e2b);
|
|
78
82
|
if (!result.ok)
|
|
79
83
|
return result;
|
|
80
84
|
// The manifest the CLI resolved enters the plan here and nowhere else; the routes read
|
|
81
85
|
// plan.study for the run's status record and bundle, and plan.warnings for its bundle events.
|
|
82
|
-
const warnings = recordedStudyWarnings(study);
|
|
86
|
+
const warnings = [...recordedStudyWarnings(study), ...(result.plan.warnings ?? [])];
|
|
83
87
|
const plan = {
|
|
84
88
|
...result.plan,
|
|
85
89
|
...(options.study === undefined ? {} : { study: options.study }),
|
|
@@ -87,7 +91,8 @@ export function planStudy(config, options, deps = {}) {
|
|
|
87
91
|
};
|
|
88
92
|
return { ok: true, planned: { plan } };
|
|
89
93
|
}
|
|
90
|
-
function planRoute(route, study, options, input, deps,
|
|
94
|
+
function planRoute(route, study, options, input, deps, e2b) {
|
|
95
|
+
const { sandboxCeiling } = e2b;
|
|
91
96
|
switch (route) {
|
|
92
97
|
case "preview":
|
|
93
98
|
return planPreview(study, options, input);
|
|
@@ -98,13 +103,13 @@ function planRoute(route, study, options, input, deps, sandboxCeiling) {
|
|
|
98
103
|
driving: callerDrivingOf(options),
|
|
99
104
|
...(options.count === undefined ? {} : { countOverride: options.count }),
|
|
100
105
|
...(options.rerun === undefined ? {} : { rerun: options.rerun }),
|
|
101
|
-
|
|
106
|
+
...e2b,
|
|
102
107
|
});
|
|
103
108
|
case "shared-world":
|
|
104
109
|
return planSharedWorldStudy(study, {
|
|
105
110
|
...input,
|
|
106
111
|
hasRunSession: deps.runSession !== undefined,
|
|
107
|
-
|
|
112
|
+
...e2b,
|
|
108
113
|
});
|
|
109
114
|
case "terminal":
|
|
110
115
|
return planTerminalStudy(study, {
|
|
@@ -113,6 +118,10 @@ function planRoute(route, study, options, input, deps, sandboxCeiling) {
|
|
|
113
118
|
sandboxCeiling,
|
|
114
119
|
});
|
|
115
120
|
case "scripted":
|
|
116
|
-
return planScriptedStudy(study, {
|
|
121
|
+
return planScriptedStudy(study, {
|
|
122
|
+
...input,
|
|
123
|
+
injectedBrowser: injectedBrowser(deps),
|
|
124
|
+
sandboxCeiling,
|
|
125
|
+
});
|
|
117
126
|
}
|
|
118
127
|
}
|
|
@@ -3,6 +3,7 @@ import type { E2BDesktopModule } from "../substrates/e2b/sdk.js";
|
|
|
3
3
|
import { type StudyConfig } from "./types.js";
|
|
4
4
|
import { STUDY_CHECK_SCHEMA } from "./preflight-result.js";
|
|
5
5
|
import { type StudyRoute } from "./plan.js";
|
|
6
|
+
import type { PlanRefusal } from "./plan-types.js";
|
|
6
7
|
import { type StudyResolveFailure } from "./discover.js";
|
|
7
8
|
export type StudyPreflightReachabilityMode = "metadata" | "public-preview" | "sandbox-loopback" | "prepared-host";
|
|
8
9
|
export interface StudyPreflightCheck {
|
|
@@ -57,7 +58,7 @@ export interface StudyPreflightResult {
|
|
|
57
58
|
spend: StudyPreflightSpend;
|
|
58
59
|
warnings: string[];
|
|
59
60
|
error?: {
|
|
60
|
-
code: StudyResolveFailure["error"]["code"] | "HUMANISH_STUDY_PREFLIGHT_INVALID_OPTION" | "HUMANISH_STUDY_PREFLIGHT_UNSUPPORTED_ROUTE" | "HUMANISH_STUDY_PREFLIGHT_TARGET_POLICY" | "HUMANISH_STUDY_PREFLIGHT_ENV_MISSING" | "HUMANISH_STUDY_PREFLIGHT_E2B_REQUIRED" | "HUMANISH_STUDY_PREFLIGHT_TARGET_UNREACHABLE" | "HUMANISH_STUDY_PREFLIGHT_PROVISION_FAILED" | "HUMANISH_STUDY_PREFLIGHT_TEARDOWN_FAILED";
|
|
61
|
+
code: StudyResolveFailure["error"]["code"] | "HUMANISH_STUDY_PREFLIGHT_INVALID_OPTION" | "HUMANISH_STUDY_PREFLIGHT_UNSUPPORTED_ROUTE" | "HUMANISH_STUDY_PREFLIGHT_TARGET_POLICY" | "HUMANISH_STUDY_PREFLIGHT_ENV_MISSING" | "HUMANISH_STUDY_PREFLIGHT_E2B_REQUIRED" | "HUMANISH_STUDY_PREFLIGHT_TARGET_UNREACHABLE" | "HUMANISH_STUDY_PREFLIGHT_PROVISION_FAILED" | "HUMANISH_STUDY_PREFLIGHT_TEARDOWN_FAILED" | PlanRefusal["code"];
|
|
61
62
|
message: string;
|
|
62
63
|
};
|
|
63
64
|
}
|
package/dist/study/preflight.js
CHANGED
|
@@ -4,8 +4,9 @@ import path from "node:path";
|
|
|
4
4
|
import { isLoopbackUrl } from "./parse/subject.js";
|
|
5
5
|
import {} from "./types.js";
|
|
6
6
|
import { runPublicPreviewPreflight, runSandboxLoopbackPreflight } from "./preflight-probes.js";
|
|
7
|
-
import { digest, finalize, STUDY_CHECK_SCHEMA } from "./preflight-result.js";
|
|
8
|
-
import { routeOf } from "./plan.js";
|
|
7
|
+
import { digest, fail, finalize, STUDY_CHECK_SCHEMA } from "./preflight-result.js";
|
|
8
|
+
import { planStudy, routeOf } from "./plan.js";
|
|
9
|
+
import { describeArrivals } from "./arrivals.js";
|
|
9
10
|
import { resolveStudyManifest } from "./discover.js";
|
|
10
11
|
import { participantList } from "./study-fields.js";
|
|
11
12
|
import { isLocalBrowserStudy } from "../substrates/local/runtime-config.js";
|
|
@@ -42,6 +43,7 @@ export async function runStudyPreflight(options) {
|
|
|
42
43
|
? new Map()
|
|
43
44
|
: (await resolveCommittedPersonasForCwd(cwd, studyPersonaIds(resolved.config))).personas;
|
|
44
45
|
const route = routeOf(resolved.config);
|
|
46
|
+
const env = options.env ?? process.env;
|
|
45
47
|
const ctx = {
|
|
46
48
|
cwd,
|
|
47
49
|
study: options.study,
|
|
@@ -52,7 +54,7 @@ export async function runStudyPreflight(options) {
|
|
|
52
54
|
route,
|
|
53
55
|
reachability,
|
|
54
56
|
timeoutMs,
|
|
55
|
-
env
|
|
57
|
+
env,
|
|
56
58
|
hooks: options.hooks ?? {},
|
|
57
59
|
checks: [
|
|
58
60
|
{ name: "study file", ok: true, message: `resolved ${resolved.origin} study file` },
|
|
@@ -65,6 +67,17 @@ export async function runStudyPreflight(options) {
|
|
|
65
67
|
...personaBackgroundWarnings(resolved.config.id, participants, personas),
|
|
66
68
|
],
|
|
67
69
|
};
|
|
70
|
+
// The plan `humanish run <study>` makes before it creates anything: no run options, so the file's
|
|
71
|
+
// mode decides dry or live, and the same environment for the sandbox ceiling. A study the run
|
|
72
|
+
// would refuse fails here with the run's code, before any probe creates a sandbox.
|
|
73
|
+
const planned = planStudy(resolved.config, { cwd, env });
|
|
74
|
+
if (!planned.ok) {
|
|
75
|
+
const { code, message } = planned.refusal;
|
|
76
|
+
return fail(ctx, code, message, [{ name: "plan", ok: false, message }]);
|
|
77
|
+
}
|
|
78
|
+
const schedule = scheduleCheck(planned.planned.plan);
|
|
79
|
+
if (schedule !== undefined)
|
|
80
|
+
ctx.checks.push(schedule);
|
|
68
81
|
const machine = machineCheck(ctx.config);
|
|
69
82
|
// A local study's app and desktops run on this machine, so the hosted probes do not apply.
|
|
70
83
|
if (reachability !== "metadata" && isLocalBrowserStudy(ctx.config))
|
|
@@ -109,6 +122,14 @@ export async function runStudyPreflight(options) {
|
|
|
109
122
|
});
|
|
110
123
|
}
|
|
111
124
|
}
|
|
125
|
+
/** The plan's schedule, for a study that declares when its participants start. */
|
|
126
|
+
function scheduleCheck(plan) {
|
|
127
|
+
if (plan.route !== "computer-use" && plan.route !== "shared-world")
|
|
128
|
+
return undefined;
|
|
129
|
+
if (!plan.arrivals.declared)
|
|
130
|
+
return undefined;
|
|
131
|
+
return { name: "schedule", ok: true, message: describeArrivals(plan.arrivals) };
|
|
132
|
+
}
|
|
112
133
|
/** What study check leaves to doctor: this machine's setup for the study, and the command. */
|
|
113
134
|
function machineCheck(config) {
|
|
114
135
|
const command = cli(`doctor --study ${config.id}`);
|
package/dist/study/routing.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { StudyConfig } from "./types.js";
|
|
2
|
-
|
|
2
|
+
/** Why a study of `count` participants cannot run, or undefined when it can. */
|
|
3
|
+
export declare function participantCountReason(count: number): string | undefined;
|
|
3
4
|
export declare function actorResolvesToComputerUse(type: string | undefined): boolean;
|
|
4
5
|
export declare function registeredComputerUseActors(): string[];
|
|
5
6
|
export declare function actorResolvesToScriptedBrowser(type: string | undefined): boolean;
|
|
@@ -9,11 +10,11 @@ export declare function registeredScriptedBrowserActors(): string[];
|
|
|
9
10
|
export declare function actorResolvesToTerminal(type: string | undefined): boolean;
|
|
10
11
|
export declare function registeredTerminalActors(): string[];
|
|
11
12
|
/**
|
|
12
|
-
* The
|
|
13
|
-
*
|
|
13
|
+
* The fan-out participant count on the computer-use route: a `participants` list's length, else
|
|
14
|
+
* the caller's count override, else a participant count, else 1. The single source of truth shared by the parser, the engine,
|
|
14
15
|
* and the pre-flight plan so the participant count is computed the same way everywhere.
|
|
15
16
|
*/
|
|
16
|
-
export declare function computerUseParticipantCount(config: StudyConfig): number;
|
|
17
|
+
export declare function computerUseParticipantCount(config: StudyConfig, countOverride?: number): number;
|
|
17
18
|
/**
|
|
18
19
|
* A participant's id: its declared roster id, else `lane-NN` (independent participants) or `role-NN`
|
|
19
20
|
* (shared-world participants) from its 0-based position. The parser's filled email recipients and
|
package/dist/study/routing.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { actorRegistry } from "../actors/registry.js";
|
|
2
2
|
import { isLoopbackUrl } from "./parse/subject.js";
|
|
3
3
|
import { participantList, declaredParticipantCount } from "./study-fields.js";
|
|
4
|
-
//
|
|
5
|
-
// and
|
|
6
|
-
|
|
4
|
+
// The most participants one study holds, on any route. One run keeps every participant's stream in
|
|
5
|
+
// one bundle, one Observer view and one analysis, and those are sized for 100: the analysis reads a
|
|
6
|
+
// bundle of at most 128 streams. How many run at once is a separate limit, the E2B plan's
|
|
7
|
+
// (concurrentSandboxes in substrates/e2b/lifetime.ts), and caps.maxUsd bounds what they spend.
|
|
8
|
+
const MAX_STUDY_PARTICIPANTS = 100;
|
|
9
|
+
/** Why a study of `count` participants cannot run, or undefined when it can. */
|
|
10
|
+
export function participantCountReason(count) {
|
|
11
|
+
return count > MAX_STUDY_PARTICIPANTS
|
|
12
|
+
? `A study runs at most ${MAX_STUDY_PARTICIPANTS} participants, and this one has ${count}. One run keeps every participant's evidence in one bundle and one Observer view, and humanish supports runs of up to ${MAX_STUDY_PARTICIPANTS}; split the participants across studies.`
|
|
13
|
+
: undefined;
|
|
14
|
+
}
|
|
7
15
|
// Run kinds whose evidence is the participant's screenshots. An actor that declares it produces none
|
|
8
16
|
// cannot run on them: a code-only actor would claim a GUI flow it only reached through a shell.
|
|
9
17
|
const SCREENSHOT_RUN_KINDS = new Set([
|
|
@@ -46,12 +54,13 @@ export function registeredTerminalActors() {
|
|
|
46
54
|
return registeredActorsOn("terminal");
|
|
47
55
|
}
|
|
48
56
|
/**
|
|
49
|
-
* The
|
|
50
|
-
*
|
|
57
|
+
* The fan-out participant count on the computer-use route: a `participants` list's length, else
|
|
58
|
+
* the caller's count override, else a participant count, else 1. The single source of truth shared by the parser, the engine,
|
|
51
59
|
* and the pre-flight plan so the participant count is computed the same way everywhere.
|
|
52
60
|
*/
|
|
53
|
-
export function computerUseParticipantCount(config) {
|
|
54
|
-
return participantList(config)?.length ??
|
|
61
|
+
export function computerUseParticipantCount(config, countOverride) {
|
|
62
|
+
return (participantList(config)?.length ??
|
|
63
|
+
Math.max(1, countOverride ?? declaredParticipantCount(config) ?? 1));
|
|
55
64
|
}
|
|
56
65
|
/**
|
|
57
66
|
* A participant's id: its declared roster id, else `lane-NN` (independent participants) or `role-NN`
|
package/dist/study/types.d.ts
CHANGED
|
@@ -319,6 +319,12 @@ export interface StudyParticipantEntry {
|
|
|
319
319
|
* into the follower missions at a host-first barrier. Inert/warned on every other route.
|
|
320
320
|
*/
|
|
321
321
|
host?: boolean;
|
|
322
|
+
/**
|
|
323
|
+
* Computer use and shared world: when this participant starts, in milliseconds after the run
|
|
324
|
+
* starts its participants (0 to 86,400,000). A group's `startEveryMs` sets it for each member.
|
|
325
|
+
* Absent, the participant starts with the run. src/study/arrivals.ts schedules the start.
|
|
326
|
+
*/
|
|
327
|
+
startAfterMs?: number;
|
|
322
328
|
}
|
|
323
329
|
export interface StudyActor {
|
|
324
330
|
/**
|
|
@@ -783,6 +789,14 @@ export interface StudyParticipantGroup {
|
|
|
783
789
|
count?: number;
|
|
784
790
|
instruction?: string;
|
|
785
791
|
}
|
|
792
|
+
/**
|
|
793
|
+
* The keys a `participants` list entry with `count` adds to an entry's. parseStudy expands the
|
|
794
|
+
* group into its members, and member k starts at `startAfterMs + (k - 1) * startEveryMs`.
|
|
795
|
+
*/
|
|
796
|
+
export interface StudyParticipantListGroup {
|
|
797
|
+
count?: number;
|
|
798
|
+
startEveryMs?: number;
|
|
799
|
+
}
|
|
786
800
|
/** The scripted surfaces: desktop, or desktop and mobile. */
|
|
787
801
|
export type StudySurfaces = readonly ["desktop"] | readonly ["desktop", "mobile"];
|
|
788
802
|
interface StudyParseSuccess {
|
|
@@ -2,8 +2,8 @@ import type { StudyConfig } from "./types.js";
|
|
|
2
2
|
/**
|
|
3
3
|
* Cross-validate the computer-use fan-out declaration (`per-lane-worlds`). Returns the failure
|
|
4
4
|
* message, or null when valid. The parser enforces it, and runStudyWith checks it again for a config
|
|
5
|
-
* that skipped the parser. It runs rosterStructuralValidationReason (id and device
|
|
6
|
-
* validity, unique ids) first, then the route-scoped XOR,
|
|
5
|
+
* that skipped the parser. It runs rosterStructuralValidationReason (roster size, id and device
|
|
6
|
+
* validity, unique ids) first, then the route-scoped XOR, participant count and policy checks.
|
|
7
7
|
*/
|
|
8
8
|
export declare function computerUseValidationReason(config: StudyConfig): string | null;
|
|
9
9
|
/**
|
package/dist/study/validation.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { isMaxOutputTokens } from "../actors/output-token-limit.js";
|
|
2
|
+
import { MAX_RECEIVING_INBOXES } from "../comms/receiving-types.js";
|
|
2
3
|
import { waitFieldsReason } from "../actors/computer-use/wait.js";
|
|
3
4
|
import { PARTICIPANT_ID_MAX_CHARS, PARTICIPANT_ID_PATTERN } from "./parse/actors.js";
|
|
4
5
|
import { isHttpUrl, isLoopbackUrl } from "./parse/subject.js";
|
|
5
6
|
import { declaredTargets } from "./plan-participants.js";
|
|
6
|
-
import { actorResolvesToComputerUse, computerUseParticipantCount,
|
|
7
|
+
import { actorResolvesToComputerUse, computerUseParticipantCount, participantCountReason, registeredComputerUseActors, resolveEntryUrl, isComputerUseComposition, isScriptedBrowserComposition, isSharedWorldComposition, isTerminalProductComposition, } from "./routing.js";
|
|
7
8
|
import { participantList } from "./study-fields.js";
|
|
9
|
+
import { isStartAfterMs, rosterStartsReason, startAfterMessage } from "./arrivals.js";
|
|
8
10
|
/**
|
|
9
11
|
* Cross-validate the computer-use fan-out declaration (`per-lane-worlds`). Returns the failure
|
|
10
12
|
* message, or null when valid. The parser enforces it, and runStudyWith checks it again for a config
|
|
11
|
-
* that skipped the parser. It runs rosterStructuralValidationReason (id and device
|
|
12
|
-
* validity, unique ids) first, then the route-scoped XOR,
|
|
13
|
+
* that skipped the parser. It runs rosterStructuralValidationReason (roster size, id and device
|
|
14
|
+
* validity, unique ids) first, then the route-scoped XOR, participant count and policy checks.
|
|
13
15
|
*/
|
|
14
16
|
export function computerUseValidationReason(config) {
|
|
15
17
|
const roster = participantList(config);
|
|
@@ -42,9 +44,9 @@ export function computerUseValidationReason(config) {
|
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
46
|
const participantCount = computerUseParticipantCount(config);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
const countReason = participantCountReason(participantCount);
|
|
48
|
+
if (countReason)
|
|
49
|
+
return countReason;
|
|
48
50
|
// Public targets fan out into N independent worlds driving the same public app, which is an
|
|
49
51
|
// ambiguous shared-world-ish shape, not a per-participant target swarm. Permit N>1 public runs only
|
|
50
52
|
// when every roster entry declares its own target, making the adapter-owned topology explicit. But when
|
|
@@ -60,8 +62,8 @@ export function computerUseValidationReason(config) {
|
|
|
60
62
|
return null;
|
|
61
63
|
}
|
|
62
64
|
/**
|
|
63
|
-
* Engine-level
|
|
64
|
-
* public TypeScript/JavaScript API instead of parseStudy.
|
|
65
|
+
* Engine-level roster validation (its size and its ids' path tokens) for configs supplied directly
|
|
66
|
+
* through the public TypeScript/JavaScript API instead of parseStudy.
|
|
65
67
|
*/
|
|
66
68
|
function rosterStructuralValidationReason(config) {
|
|
67
69
|
const roster = participantList(config);
|
|
@@ -70,10 +72,16 @@ function rosterStructuralValidationReason(config) {
|
|
|
70
72
|
if (!Array.isArray(roster) || roster.length === 0) {
|
|
71
73
|
return "participants must be a non-empty array when set.";
|
|
72
74
|
}
|
|
75
|
+
const countReason = participantCountReason(roster.length);
|
|
76
|
+
if (countReason)
|
|
77
|
+
return countReason;
|
|
73
78
|
for (const [index, entry] of roster.entries()) {
|
|
74
79
|
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
75
80
|
return `participants[${index}] must be an object.`;
|
|
76
81
|
}
|
|
82
|
+
if (entry.startAfterMs !== undefined && !isStartAfterMs(entry.startAfterMs)) {
|
|
83
|
+
return startAfterMessage(`participants[${index}].startAfterMs`);
|
|
84
|
+
}
|
|
77
85
|
const id = entry.id;
|
|
78
86
|
if (id === undefined) {
|
|
79
87
|
continue;
|
|
@@ -88,6 +96,7 @@ function rosterStructuralValidationReason(config) {
|
|
|
88
96
|
}
|
|
89
97
|
seenIds.add(id);
|
|
90
98
|
}
|
|
99
|
+
return rosterStartsReason(roster) ?? null;
|
|
91
100
|
}
|
|
92
101
|
return null;
|
|
93
102
|
}
|
|
@@ -195,6 +204,10 @@ export function receivingEmailValidationReason(config) {
|
|
|
195
204
|
!["app-url", "clone", "local-tree"].includes(config.subject.source)) {
|
|
196
205
|
return "Real email receiving requires a hosted computer-use browser study with an app-url, clone, or local-tree subject. Scripted, terminal, desktop-cli and local-app routes are unsupported.";
|
|
197
206
|
}
|
|
207
|
+
const participants = computerUseParticipantCount(config);
|
|
208
|
+
if (participants > MAX_RECEIVING_INBOXES) {
|
|
209
|
+
return `Real email receiving leases one inbox per participant, at most ${MAX_RECEIVING_INBOXES} per run, and this study has ${participants} participants. Run at most ${MAX_RECEIVING_INBOXES} participants with a real inbox, or split them across studies.`;
|
|
210
|
+
}
|
|
198
211
|
if (config.actor?.type === "local-agent") {
|
|
199
212
|
return "Real email receiving is unavailable for local-agent: its host process does not isolate the inbox management credential. Use a hosted first-party computer-use actor.";
|
|
200
213
|
}
|
|
@@ -245,14 +258,14 @@ export function waitLimitValidationReason(config) {
|
|
|
245
258
|
}
|
|
246
259
|
/**
|
|
247
260
|
* Shared-world participants share one live app, so at least two must be live at once. The
|
|
248
|
-
* sequential shared-world route (`execution.concurrency: 1`) was removed in 0.106.0
|
|
249
|
-
*
|
|
250
|
-
* two-participant roster floor, so a one-participant roster gets the
|
|
251
|
-
* one.
|
|
261
|
+
* sequential shared-world route (`execution.concurrency: 1`) was removed in 0.106.0. An omitted
|
|
262
|
+
* concurrency runs every participant, up to the E2B plan's limit, which the planner checks. Both
|
|
263
|
+
* callers check it after their two-participant roster floor, so a one-participant roster gets the
|
|
264
|
+
* roster refusal, never this one.
|
|
252
265
|
*/
|
|
253
266
|
function sharedWorldConcurrencyReason(config) {
|
|
254
|
-
//
|
|
255
|
-
//
|
|
267
|
+
// An omitted value runs every participant, so it is checked as the participant count. A missing
|
|
268
|
+
// roster reads as 0 and is refused.
|
|
256
269
|
const participants = participantList(config)?.length ?? 0;
|
|
257
270
|
const concurrency = config.execution?.concurrency ?? participants;
|
|
258
271
|
if (concurrency >= 2)
|
package/dist/study/warnings.js
CHANGED
|
@@ -302,11 +302,19 @@ export function forwardDeclaredWarnings(config) {
|
|
|
302
302
|
];
|
|
303
303
|
// A declared cap below the participant count is legal but loud: the roster promises N live actors
|
|
304
304
|
// and the cap delivers waves of M. Say so up front (inspect + dry-run + run): a green run in
|
|
305
|
-
// waves is otherwise indistinguishable from the all-live run the author meant.
|
|
305
|
+
// waves is otherwise indistinguishable from the all-live run the author meant. A roster that
|
|
306
|
+
// declares when its participants start never promised them all at once; the plan's schedule
|
|
307
|
+
// says who waits for a slot.
|
|
306
308
|
{
|
|
307
|
-
const
|
|
309
|
+
const roster = participantList(config);
|
|
310
|
+
const participantCount = roster?.length ?? declaredParticipantCount(config) ?? 1;
|
|
308
311
|
const cap = config.execution?.concurrency;
|
|
309
|
-
|
|
312
|
+
const scheduled = roster?.some((entry) => entry.startAfterMs !== undefined) === true;
|
|
313
|
+
if (routes.cua &&
|
|
314
|
+
!scheduled &&
|
|
315
|
+
cap !== undefined &&
|
|
316
|
+
participantCount > 1 &&
|
|
317
|
+
cap < participantCount) {
|
|
310
318
|
warnings.push(`execution.concurrency ${cap} caps a ${participantCount}-participant roster: participants run in waves of ${cap}, never all live at once. Remove execution.concurrency (the default runs all ${participantCount} participants simultaneously) or set it to ${participantCount}; declare a lower cap only to bound simultaneous paid desktops.`);
|
|
311
319
|
}
|
|
312
320
|
}
|
|
@@ -24,9 +24,80 @@ export declare function describeSandboxCeiling(ceiling: SandboxCeiling): string;
|
|
|
24
24
|
* setting value that would admit it, when an E2B plan allows that long.
|
|
25
25
|
*/
|
|
26
26
|
export declare function ceilingAdvice(deadlineMs: number): string;
|
|
27
|
-
/**
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
/**
|
|
28
|
+
* How many sandboxes humanish runs at once, and whether it is the default or the setting; or why
|
|
29
|
+
* the setting cannot be read.
|
|
30
|
+
*/
|
|
31
|
+
export type ConcurrentSandboxes = {
|
|
32
|
+
readonly ok: true;
|
|
33
|
+
readonly count: number;
|
|
34
|
+
readonly source: "default" | "setting";
|
|
35
|
+
} | {
|
|
36
|
+
readonly ok: false;
|
|
37
|
+
readonly message: string;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* The concurrent sandbox limit: HUMANISH_E2B_MAX_CONCURRENT_SANDBOXES when set, else 20. E2B
|
|
41
|
+
* refuses a create past the plan's limit, and neither its API nor its SDK says which plan a key is
|
|
42
|
+
* on, so the operator states it, as for the sandbox ceiling. A study that would run more desktops
|
|
43
|
+
* at once is refused or run in waves at plan time, before a create fails mid-run.
|
|
44
|
+
*/
|
|
45
|
+
export declare function concurrentSandboxes(env: Readonly<Record<string, string | undefined>>): ConcurrentSandboxes;
|
|
46
|
+
/** The limit in use and where it came from, or why the setting cannot be read (doctor's row). */
|
|
47
|
+
export declare function describeConcurrentSandboxes(limit: ConcurrentSandboxes): string;
|
|
48
|
+
/**
|
|
49
|
+
* How many participants a hosted study runs at once: its `concurrency` (the declared
|
|
50
|
+
* execution.concurrency clamped to the roster, else every participant), lowered to what the plan
|
|
51
|
+
* runs. `declared` says whether the study set it. `alongside` counts the sandboxes the study keeps
|
|
52
|
+
* for the whole run besides its participants' desktops, such as a shared world's app, and
|
|
53
|
+
* `minimum` is the fewest at once the route can run. A declared value that does not fit is a
|
|
54
|
+
* refusal naming the setting value that admits it. `lowered` is the warning a plan records when
|
|
55
|
+
* the limit holds an undeclared default to waves.
|
|
56
|
+
*/
|
|
57
|
+
export declare function participantsAtOnce(study: {
|
|
58
|
+
readonly concurrency: number;
|
|
59
|
+
readonly declared: boolean;
|
|
60
|
+
readonly participants: number;
|
|
61
|
+
readonly alongside?: number;
|
|
62
|
+
readonly minimum?: number;
|
|
63
|
+
}, limit: ConcurrentSandboxes): {
|
|
64
|
+
readonly ok: true;
|
|
65
|
+
readonly count: number;
|
|
66
|
+
readonly lowered?: string;
|
|
67
|
+
} | {
|
|
68
|
+
readonly ok: false;
|
|
69
|
+
readonly message: string;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* What a sandbox's server-side timeout adds to the session it hosts: the teardown buffer, and on a
|
|
73
|
+
* sandbox that serves the subject, the provisioning budget and each seed step's budget. A planner
|
|
74
|
+
* adds it to the session to get the deadline it checks against the ceiling, and the route adds it
|
|
75
|
+
* to set the timeout it asks E2B for.
|
|
76
|
+
*/
|
|
77
|
+
export declare function sandboxHeadroomMs(servedSubject?: {
|
|
78
|
+
readonly seed: readonly {
|
|
79
|
+
readonly timeoutMs?: number | undefined;
|
|
80
|
+
}[];
|
|
81
|
+
}): number;
|
|
82
|
+
/** A sandbox a plan will ask E2B for, as its deadline refusal describes it. */
|
|
83
|
+
export interface PlannedSandbox {
|
|
84
|
+
/** How the refusal names the sandbox, as in "the subject sandbox". */
|
|
85
|
+
readonly name: string;
|
|
86
|
+
/** The session it hosts, and whether the study set it in execution.timeoutMs. */
|
|
87
|
+
readonly sessionMs: number;
|
|
88
|
+
readonly sessionDeclared: boolean;
|
|
89
|
+
/** The subject it serves, whose provisioning and seed steps add to its deadline. */
|
|
90
|
+
readonly servedSubject?: {
|
|
91
|
+
readonly seed: readonly {
|
|
92
|
+
readonly timeoutMs?: number | undefined;
|
|
93
|
+
}[];
|
|
94
|
+
} | undefined;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Why the sandbox's deadline, its session plus sandboxHeadroomMs, passes the `ceilingMs` ceiling,
|
|
98
|
+
* or undefined when it fits. The message shows the arithmetic, the longest execution.timeoutMs the
|
|
99
|
+
* ceiling leaves, and the setting value that would admit the study.
|
|
100
|
+
*/
|
|
101
|
+
export declare function sandboxDeadlineRefusal(sandbox: PlannedSandbox, ceilingMs: number): string | undefined;
|
|
31
102
|
/** The timeout of one E2B API request: HUMANISH_E2B_REQUEST_TIMEOUT_MS when set, else 60 s. */
|
|
32
103
|
export declare function e2bRequestTimeoutMs(env: Record<string, string | undefined>): number;
|