humanish 0.110.3 → 0.111.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 +29 -0
- package/CONTEXT.md +2 -2
- package/dist/actors/local-agent/claude-session.js +1 -1
- package/dist/actors/local-agent/cli.js +1 -1
- package/dist/actors/scripted-browser/actor.d.ts +2 -2
- package/dist/actors/scripted-browser/actor.js +17 -8
- package/dist/actors/scripted-browser/launch-env.d.ts +12 -0
- package/dist/actors/scripted-browser/launch-env.js +50 -0
- package/dist/analysis/automatic-completion.d.ts +2 -1
- package/dist/analysis/automatic-completion.js +7 -2
- package/dist/cli/commands/migrate.js +1 -1
- package/dist/cli/commands/runs.js +5 -2
- package/dist/cli/commands/study-route-computer-use.js +3 -2
- package/dist/cli/commands/study-route-preview.js +2 -1
- package/dist/cli/commands/study-route-shared-world.js +3 -3
- package/dist/cli/commands/study.js +2 -1
- package/dist/routes/computer-use/live-phase.js +8 -1
- package/dist/routes/computer-use/local-vm.js +2 -1
- package/dist/routes/computer-use/participant-model.js +1 -1
- package/dist/routes/computer-use/plan.js +9 -10
- package/dist/routes/computer-use/preflight.js +1 -1
- package/dist/routes/computer-use/result.js +1 -1
- package/dist/routes/computer-use/route.js +3 -2
- package/dist/routes/computer-use/setup.js +2 -1
- package/dist/routes/computer-use/types.d.ts +4 -4
- package/dist/routes/scripted/plan.js +5 -4
- package/dist/routes/scripted/route.js +6 -2
- package/dist/routes/scripted/scenario.d.ts +1 -1
- package/dist/routes/scripted/scenario.js +3 -3
- package/dist/routes/scripted/surface-sessions.d.ts +2 -0
- package/dist/routes/scripted/surface-sessions.js +2 -1
- package/dist/routes/scripted/types.d.ts +2 -2
- package/dist/routes/shared-world/handoff.js +1 -1
- package/dist/routes/shared-world/participant-specs.js +6 -2
- package/dist/routes/shared-world/plan.js +8 -8
- package/dist/routes/shared-world/route.js +3 -3
- package/dist/routes/shared-world/types.d.ts +1 -1
- package/dist/routes/terminal/bundle.d.ts +3 -3
- package/dist/routes/terminal/ledger.d.ts +2 -2
- package/dist/routes/terminal/lifetime.js +1 -1
- package/dist/routes/terminal/live-finish.js +1 -1
- package/dist/routes/terminal/plan.js +4 -3
- package/dist/routes/terminal/route.js +3 -2
- package/dist/routes/terminal/session.js +1 -0
- package/dist/routes/terminal/types.d.ts +1 -1
- package/dist/run/display.d.ts +10 -0
- package/dist/run/display.js +22 -0
- package/dist/run/reclaim.d.ts +4 -2
- package/dist/run/reclaim.js +61 -18
- package/dist/run/results.d.ts +1 -1
- package/dist/run/run.d.ts +11 -1
- package/dist/run/run.js +24 -5
- package/dist/run/status.d.ts +6 -0
- package/dist/run/status.js +1 -0
- package/dist/run/stored-runs.js +26 -11
- package/dist/run-study.d.ts +5 -3
- package/dist/run-study.js +7 -11
- package/dist/study/adapter-scorer-loader.d.ts +1 -1
- package/dist/study/adapter-scorer-loader.js +3 -3
- package/dist/study/composition-rules.js +49 -39
- package/dist/study/config.d.ts +31 -8
- package/dist/study/config.js +181 -155
- package/dist/study/discover.d.ts +1 -1
- package/dist/study/discover.js +1 -1
- package/dist/study/doctor.js +5 -4
- package/dist/study/keys.d.ts +250 -2
- package/dist/study/keys.js +32 -62
- package/dist/study/{convert.d.ts → migrate/convert.d.ts} +1 -1
- package/dist/study/{convert.js → migrate/convert.js} +28 -31
- package/dist/study/{migrate.d.ts → migrate/migrate.d.ts} +1 -1
- package/dist/study/{migrate.js → migrate/migrate.js} +2 -2
- package/dist/study/{owned-files.js → migrate/owned-files.js} +1 -1
- package/dist/study/migrate/v2.d.ts +78 -0
- package/dist/study/migrate/v2.js +607 -0
- package/dist/study/parse/actors.d.ts +34 -10
- package/dist/study/parse/actors.js +76 -174
- package/dist/study/parse/comms.js +6 -2
- package/dist/study/parse/execution.d.ts +9 -4
- package/dist/study/parse/execution.js +6 -41
- package/dist/study/parse/front.d.ts +36 -0
- package/dist/study/parse/{study-v3.js → front.js} +37 -88
- package/dist/study/parse/subject.d.ts +2 -0
- package/dist/study/parse/subject.js +15 -18
- package/dist/study/persona-resolve.d.ts +2 -4
- package/dist/study/persona-resolve.js +7 -8
- package/dist/study/plan-base.d.ts +1 -1
- package/dist/study/plan-base.js +6 -6
- package/dist/study/plan-participants.d.ts +2 -2
- package/dist/study/plan-participants.js +12 -11
- package/dist/study/plan-types.d.ts +5 -5
- package/dist/study/plan.js +5 -4
- package/dist/study/preflight-probes.js +4 -4
- package/dist/study/preflight.d.ts +1 -1
- package/dist/study/preflight.js +3 -3
- package/dist/study/routing.d.ts +5 -5
- package/dist/study/routing.js +17 -20
- package/dist/study/run-study-options.d.ts +1 -1
- package/dist/study/run-study-options.js +3 -3
- package/dist/study/study-deps.d.ts +1 -1
- package/dist/study/study-fields.d.ts +19 -0
- package/dist/study/study-fields.js +46 -0
- package/dist/study/summary.js +19 -18
- package/dist/study/types.d.ts +66 -104
- package/dist/study/types.js +0 -1
- package/dist/study/url-credentials.js +3 -3
- package/dist/study/validation.d.ts +4 -10
- package/dist/study/validation.js +54 -79
- package/dist/study/warnings.d.ts +3 -3
- package/dist/study/warnings.js +42 -110
- package/dist/substrates/local/runtime-config.js +14 -16
- package/dist/tui-app.js +3 -3
- package/docs/contracts/schemas.md +36 -8
- package/package.json +1 -1
- package/dist/study/parse/study-v3.d.ts +0 -21
- /package/dist/study/{owned-files.d.ts → migrate/owned-files.d.ts} +0 -0
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
// humanish.study.v3
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// route against the one the config takes, and refuses any field that route does not read.
|
|
1
|
+
// What a humanish.study.v3 document declares before its sections parse: its route, one `actor`,
|
|
2
|
+
// its `participants` or `surfaces`, and the `caps` keys the route reads. config.ts parses the
|
|
3
|
+
// sections after these checks pass.
|
|
5
4
|
import { isRecord } from "../../run/type-guards.js";
|
|
6
5
|
import { findUnknownStudyKey } from "../keys.js";
|
|
7
|
-
import { V2_SCHEMA } from "../types.js";
|
|
8
6
|
import { PARTICIPANT_ID_MAX_CHARS, PARTICIPANT_ID_PATTERN } from "./actors.js";
|
|
9
7
|
import { invalid, posInt } from "./values.js";
|
|
10
8
|
// The values `route:` takes: every route routeOf can return.
|
|
@@ -24,8 +22,23 @@ const CAPS_KEYS = {
|
|
|
24
22
|
terminal: ["maxUsd", "maxJobs", "maxMinutes"],
|
|
25
23
|
scripted: [],
|
|
26
24
|
};
|
|
27
|
-
/**
|
|
28
|
-
export
|
|
25
|
+
/** The refusal for a study with no `actor` object. */
|
|
26
|
+
export const ACTOR_REQUIRED = "A study needs `actor:`, an object with at least `type`.";
|
|
27
|
+
/** Why `route` is not a route a study can declare, or undefined when it is one. */
|
|
28
|
+
export function routeNameReason(route) {
|
|
29
|
+
return ROUTES.some((name) => name === route)
|
|
30
|
+
? undefined
|
|
31
|
+
: `A study needs \`route:\`, one of ${ROUTES.join(", ")}.`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The checks a v3 document passes before its sections parse: no v2 key and no unknown key, a
|
|
35
|
+
* route, a mode, an `actor` object, a scenario string, and the `participants`, `surfaces` and
|
|
36
|
+
* `caps` keys its route takes. `anySharedWorldParticipants`, for migrate's v3 form of a v2 file:
|
|
37
|
+
* a shared world takes `participants` in every form computer use takes, or none, because a v2
|
|
38
|
+
* shared world could declare a count or no list. The shared-world checks then refuse it, as the v2
|
|
39
|
+
* parser does.
|
|
40
|
+
*/
|
|
41
|
+
export function readStudyFront(raw, anySharedWorldParticipants = false) {
|
|
29
42
|
const moved = movedKeyReason(raw);
|
|
30
43
|
if (moved)
|
|
31
44
|
return invalid(moved);
|
|
@@ -33,90 +46,33 @@ export function studyToV2(raw) {
|
|
|
33
46
|
if (unknownKey)
|
|
34
47
|
return invalid(unknownKey);
|
|
35
48
|
const route = ROUTES.find((name) => name === raw.route);
|
|
36
|
-
if (route === undefined)
|
|
37
|
-
return invalid(
|
|
38
|
-
}
|
|
49
|
+
if (route === undefined)
|
|
50
|
+
return invalid(routeNameReason(raw.route));
|
|
39
51
|
if (raw.mode !== undefined && raw.mode !== "dry-run" && raw.mode !== "live") {
|
|
40
52
|
return invalid("`mode` must be `dry-run` or `live`. Leave it out for a dry run.");
|
|
41
53
|
}
|
|
42
|
-
if (!isRecord(raw.actor))
|
|
43
|
-
return invalid(
|
|
44
|
-
}
|
|
54
|
+
if (!isRecord(raw.actor))
|
|
55
|
+
return invalid(ACTOR_REQUIRED);
|
|
45
56
|
if (raw.scenario !== undefined && typeof raw.scenario !== "string") {
|
|
46
57
|
return invalid("`scenario` is a scenario id or a path to one, as a string.");
|
|
47
58
|
}
|
|
48
|
-
const participants = participantsOf(route, raw.participants);
|
|
59
|
+
const participants = participantsOf(anySharedWorldParticipants && route === "shared-world" ? "computer-use" : route, raw.participants);
|
|
49
60
|
if (!participants.ok)
|
|
50
61
|
return participants;
|
|
51
|
-
const surfaces =
|
|
62
|
+
const surfaces = surfacesOf(route, raw.surfaces);
|
|
52
63
|
if (!surfaces.ok)
|
|
53
64
|
return surfaces;
|
|
54
65
|
const capsReason = capsKeysReason(route, raw.caps);
|
|
55
66
|
if (capsReason)
|
|
56
67
|
return invalid(capsReason);
|
|
57
|
-
// The
|
|
68
|
+
// The inert-field rows (warnings.ts) do not list this one; the terminal planner never reads it.
|
|
58
69
|
if (route === "terminal" && isRecord(raw.execution) && raw.execution.timeoutMs !== undefined) {
|
|
59
70
|
return invalid("route: terminal does not read `execution.timeoutMs`: `caps.maxMinutes` is the command deadline. Remove it.");
|
|
60
71
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
["laneFocus", instruction === undefined ? undefined : { instruction }],
|
|
66
|
-
["lanes", entries],
|
|
67
|
-
];
|
|
68
|
-
const actor = { ...raw.actor };
|
|
69
|
-
for (const [key, value] of v2Participants)
|
|
70
|
-
if (value !== undefined)
|
|
71
|
-
actor[key] = value;
|
|
72
|
-
const caps = raw.caps;
|
|
73
|
-
const scenario = {};
|
|
74
|
-
if (raw.scenario !== undefined)
|
|
75
|
-
scenario.ref = raw.scenario;
|
|
76
|
-
if (raw.mode !== undefined)
|
|
77
|
-
scenario.mode = raw.mode;
|
|
78
|
-
if (caps !== undefined && route === "terminal")
|
|
79
|
-
scenario.caps = caps;
|
|
80
|
-
const execution = caps !== undefined && route !== "terminal"
|
|
81
|
-
? withKey(raw.execution, "caps", caps)
|
|
82
|
-
: raw.execution;
|
|
83
|
-
const subject = route === "shared-world" ? withKey(raw.subject, "topology", "shared-world") : raw.subject;
|
|
84
|
-
const v2 = { schema: V2_SCHEMA };
|
|
85
|
-
const fields = [
|
|
86
|
-
["id", raw.id],
|
|
87
|
-
["title", raw.title],
|
|
88
|
-
["description", raw.description],
|
|
89
|
-
["subject", subject],
|
|
90
|
-
["actors", [actor]],
|
|
91
|
-
["execution", execution],
|
|
92
|
-
["scenario", Object.keys(scenario).length > 0 ? scenario : undefined],
|
|
93
|
-
["policies", raw.policies],
|
|
94
|
-
["review", raw.review],
|
|
95
|
-
["defaults", raw.defaults],
|
|
96
|
-
["comms", raw.comms],
|
|
97
|
-
];
|
|
98
|
-
for (const [key, value] of fields)
|
|
99
|
-
if (value !== undefined)
|
|
100
|
-
v2[key] = value;
|
|
101
|
-
return { ok: true, value: { route, v2, participantSource: source } };
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Spell a v2 parser message the way the study file is written: `actors[0].lanes[3]` becomes the
|
|
105
|
-
* `participants` entry it came from, `execution.caps` and `scenario.caps` become `caps`, and so on.
|
|
106
|
-
*/
|
|
107
|
-
export function studySpelling(message, route, participantSource) {
|
|
108
|
-
return (message
|
|
109
|
-
.replaceAll(/actors\[0\]\.lanes\[(\d+)\]/g, (_match, index) => `participants[${participantSource[Number(index)] ?? index}]`)
|
|
110
|
-
// "an `actors[0].lanes` roster" reads "a `participants` list" in a v3 file.
|
|
111
|
-
.replaceAll(/\ban `actors\[0\]\.(lanes|roster)` roster\b/g, "a `participants` list")
|
|
112
|
-
.replaceAll(/actors\[0\]\.(lanes|roster)\b/g, "participants")
|
|
113
|
-
.replaceAll(/actors\[0\]\.laneFocus(\.instruction)?/g, "participants.instruction")
|
|
114
|
-
.replaceAll(/actors\[0\]\.count\b/g, route === "scripted" ? "surfaces" : "participants")
|
|
115
|
-
.replaceAll("actors[0]", "actor")
|
|
116
|
-
.replaceAll(/\b(execution|scenario)\.caps\b/g, "caps")
|
|
117
|
-
.replaceAll(/\bscenario\.mode\b/g, "mode")
|
|
118
|
-
.replaceAll(/\bscenario\.ref\b/g, "scenario")
|
|
119
|
-
.replaceAll("subject.topology: shared-world", "route: shared-world"));
|
|
72
|
+
return {
|
|
73
|
+
ok: true,
|
|
74
|
+
value: { route, actor: raw.actor, participants: participants.value, surfaces: surfaces.value },
|
|
75
|
+
};
|
|
120
76
|
}
|
|
121
77
|
// The v2 keys a hand-converted file is most likely to keep, each with where it went.
|
|
122
78
|
function movedKeyReason(raw) {
|
|
@@ -194,7 +150,7 @@ function homogeneousOf(raw) {
|
|
|
194
150
|
return { ok: true, value: participants };
|
|
195
151
|
}
|
|
196
152
|
// A list entry with `count: n` is a group: n participants `<id>-01` to `<id>-NN`, even when n is 1.
|
|
197
|
-
//
|
|
153
|
+
// config.ts then checks every entry and the expanded ids for collisions.
|
|
198
154
|
function entriesOf(raw) {
|
|
199
155
|
const entries = [];
|
|
200
156
|
const source = [];
|
|
@@ -223,17 +179,17 @@ function entriesOf(raw) {
|
|
|
223
179
|
}
|
|
224
180
|
return { ok: true, value: { entries, source } };
|
|
225
181
|
}
|
|
226
|
-
|
|
227
|
-
function surfaceCount(route, raw) {
|
|
182
|
+
function surfacesOf(route, raw) {
|
|
228
183
|
if (raw === undefined)
|
|
229
184
|
return { ok: true, value: undefined };
|
|
230
185
|
if (route !== "scripted") {
|
|
231
186
|
return invalid(`\`surfaces\` belongs to route: scripted. This study's route is ${route}; remove it.`);
|
|
232
187
|
}
|
|
233
|
-
if (Array.isArray(raw) && raw.length === 1 && raw[0] === "desktop")
|
|
234
|
-
return { ok: true, value:
|
|
188
|
+
if (Array.isArray(raw) && raw.length === 1 && raw[0] === "desktop") {
|
|
189
|
+
return { ok: true, value: ["desktop"] };
|
|
190
|
+
}
|
|
235
191
|
if (Array.isArray(raw) && raw.length === 2 && raw[0] === "desktop" && raw[1] === "mobile") {
|
|
236
|
-
return { ok: true, value:
|
|
192
|
+
return { ok: true, value: ["desktop", "mobile"] };
|
|
237
193
|
}
|
|
238
194
|
return invalid("`surfaces` must be [desktop] or [desktop, mobile].");
|
|
239
195
|
}
|
|
@@ -250,10 +206,3 @@ function capsKeysReason(route, raw) {
|
|
|
250
206
|
return undefined;
|
|
251
207
|
return `route: ${route} reads only ${read.map((key) => `caps.${key}`).join(", ")}. Remove ${unread.map((key) => `caps.${key}`).join(", ")}.`;
|
|
252
208
|
}
|
|
253
|
-
// Add one key to a section. A section that is not an object passes through unchanged, so the v2
|
|
254
|
-
// parser refuses it with its own message.
|
|
255
|
-
function withKey(section, key, value) {
|
|
256
|
-
if (section === undefined)
|
|
257
|
-
return { [key]: value };
|
|
258
|
-
return isRecord(section) ? { ...section, [key]: value } : section;
|
|
259
|
-
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { StudyParseFailure, StudySubject } from "../types.js";
|
|
2
|
+
/** True when `raw` is a subject mapping whose `source` parseSubject accepts. */
|
|
3
|
+
export declare function hasSubjectSource(raw: unknown): boolean;
|
|
2
4
|
export declare function parseSubject(raw: unknown): {
|
|
3
5
|
ok: true;
|
|
4
6
|
value: StudySubject;
|
|
@@ -3,31 +3,28 @@ import { ENV_NAME_PATTERN, invalid, posInt, str, strList } from "./values.js";
|
|
|
3
3
|
import { parseEnvValues, parseState, subjectStateInvalidReason } from "./subject-state.js";
|
|
4
4
|
import { isRecord } from "../../run/type-guards.js";
|
|
5
5
|
import { urlCredentialReason } from "./url-credentials.js";
|
|
6
|
+
const SOURCES = [
|
|
7
|
+
"this-repo",
|
|
8
|
+
"clone",
|
|
9
|
+
"app-url",
|
|
10
|
+
"local-app",
|
|
11
|
+
"desktop-cli",
|
|
12
|
+
"terminal-product",
|
|
13
|
+
"local-tree",
|
|
14
|
+
];
|
|
15
|
+
/** True when `raw` is a subject mapping whose `source` parseSubject accepts. */
|
|
16
|
+
export function hasSubjectSource(raw) {
|
|
17
|
+
return isRecord(raw) && SOURCES.some((name) => name === str(raw.source));
|
|
18
|
+
}
|
|
6
19
|
export function parseSubject(raw) {
|
|
7
20
|
if (!isRecord(raw)) {
|
|
8
21
|
return invalid("`subject` is required and must be an object.");
|
|
9
22
|
}
|
|
10
|
-
const source = str(raw.source);
|
|
11
|
-
if (source
|
|
12
|
-
source !== "clone" &&
|
|
13
|
-
source !== "app-url" &&
|
|
14
|
-
source !== "local-app" &&
|
|
15
|
-
source !== "desktop-cli" &&
|
|
16
|
-
source !== "terminal-product" &&
|
|
17
|
-
source !== "local-tree") {
|
|
23
|
+
const source = SOURCES.find((name) => name === str(raw.source));
|
|
24
|
+
if (source === undefined) {
|
|
18
25
|
return invalid("`subject.source` must be one of: this-repo, clone, app-url, local-app, terminal-product, desktop-cli, local-tree.");
|
|
19
26
|
}
|
|
20
27
|
const subject = { source };
|
|
21
|
-
// topology is enum-validated everywhere; its semantics (shared-world requires clone × e2b-desktop
|
|
22
|
-
// × a ≥2 roster) are enforced in the shared-world cross-validation below, and a set-but-unconsumed
|
|
23
|
-
// topology warns as inert off the shared-world route.
|
|
24
|
-
if (raw.topology !== undefined) {
|
|
25
|
-
const topology = str(raw.topology);
|
|
26
|
-
if (topology !== "per-lane-worlds" && topology !== "shared-world") {
|
|
27
|
-
return invalid("`subject.topology` must be per-lane-worlds (the default) or shared-world.");
|
|
28
|
-
}
|
|
29
|
-
subject.topology = topology;
|
|
30
|
-
}
|
|
31
28
|
// exposure is enum-validated everywhere; it is required on the concurrent shared-world route (the
|
|
32
29
|
// getHost synthetic-subject attestation) and warns inert elsewhere.
|
|
33
30
|
if (raw.exposure !== undefined) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ResolvedPersona } from "./persona.js";
|
|
2
2
|
import { type PreparedSelectedOutputDirectory } from "../run/contained-output.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { StudyConfig } from "./types.js";
|
|
4
4
|
/** Title-case an id for the fallback display name (`skeptical-power-user` -> `Skeptical Power User`). */
|
|
5
5
|
export declare function personaTitleFromId(personaId: string): string;
|
|
6
6
|
/**
|
|
@@ -25,9 +25,7 @@ export declare function resolveCommittedPersonas(projectRoot: PreparedSelectedOu
|
|
|
25
25
|
* Every persona id a study config could put on a browser participant: the roster when one is
|
|
26
26
|
* declared, otherwise the actor-level persona that every fan-out participant inherits.
|
|
27
27
|
*/
|
|
28
|
-
export declare function studyPersonaIds(config:
|
|
29
|
-
actors?: readonly Pick<StudyActor, "persona" | "lanes">[];
|
|
30
|
-
}): string[];
|
|
28
|
+
export declare function studyPersonaIds(config: StudyConfig): string[];
|
|
31
29
|
/**
|
|
32
30
|
* Same resolution from a plain cwd, for the labs that carry a directory string rather than an
|
|
33
31
|
* already-prepared root. Realpath-then-prepare mirrors the computer-use route so a symlinked cwd
|
|
@@ -5,7 +5,7 @@ import { parseResolvedPersona, PersonaConfigError } from "./persona.js";
|
|
|
5
5
|
import { prepareSelectedOutputDirectory, readContainedRegularFile, } from "../run/contained-output.js";
|
|
6
6
|
import { digestText, redactText } from "../evidence/redaction.js";
|
|
7
7
|
import { realpath } from "node:fs/promises";
|
|
8
|
-
import {
|
|
8
|
+
import { actorOf, participantList } from "./study-fields.js";
|
|
9
9
|
/** Persona ids are file-name segments, never paths: the same grammar the terminal route enforces. */
|
|
10
10
|
const PERSONA_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
11
11
|
/** Title-case an id for the fallback display name (`skeptical-power-user` -> `Skeptical Power User`). */
|
|
@@ -86,13 +86,12 @@ export async function resolveCommittedPersonas(projectRoot, personaIds) {
|
|
|
86
86
|
*/
|
|
87
87
|
export function studyPersonaIds(config) {
|
|
88
88
|
const ids = [];
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
89
|
+
const persona = actorOf(config)?.persona;
|
|
90
|
+
if (persona)
|
|
91
|
+
ids.push(persona);
|
|
92
|
+
for (const entry of participantList(config) ?? []) {
|
|
93
|
+
if (entry.persona)
|
|
94
|
+
ids.push(entry.persona);
|
|
96
95
|
}
|
|
97
96
|
return [...new Set(ids)];
|
|
98
97
|
}
|
|
@@ -2,7 +2,7 @@ import type { Brain, ComputerUsePlan, NonEmpty, PlannedAnalysis, ProvisionedSubj
|
|
|
2
2
|
import type { StudyConfig } from "./types.js";
|
|
3
3
|
export type Base = Omit<ComputerUsePlan, "route" | "actor" | "runner" | "concurrency" | "sessionBudgetMs" | "sandboxMs" | "caps" | "rerun">;
|
|
4
4
|
export declare function isNonEmpty<T>(values: readonly T[]): values is NonEmpty<T>;
|
|
5
|
-
export declare function
|
|
5
|
+
export declare function planCaps(config: StudyConfig): ComputerUsePlan["caps"];
|
|
6
6
|
export declare function provisionedSubject(config: StudyConfig): ProvisionedSubject | undefined;
|
|
7
7
|
/** Keys, env and local tools a live run checks right before it acquires anything. */
|
|
8
8
|
export declare function desktopRequirements(config: StudyConfig, args: {
|
package/dist/study/plan-base.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// What every route planner shares: the plan fields common to all routes, the frozen residual config,
|
|
2
2
|
// and the requirement and subject helpers.
|
|
3
3
|
import { DEFAULT_OPENAI_CU_MODEL } from "../actors/computer-use/openai-provider.js";
|
|
4
|
+
import { actorOf, capsOf } from "./study-fields.js";
|
|
4
5
|
// A YAML alias can make a parsed record contain itself (an inline persona that names its own
|
|
5
6
|
// anchor), and structuredClone keeps the cycle, so each object is frozen once.
|
|
6
7
|
function deepFreeze(value, frozen = new WeakSet()) {
|
|
@@ -13,11 +14,10 @@ function deepFreeze(value, frozen = new WeakSet()) {
|
|
|
13
14
|
return value;
|
|
14
15
|
}
|
|
15
16
|
function residualOf(config) {
|
|
16
|
-
const { comms, policies,
|
|
17
|
+
const { comms, policies, defaults, review } = config;
|
|
17
18
|
return deepFreeze(structuredClone({
|
|
18
19
|
...(comms === undefined ? {} : { comms }),
|
|
19
20
|
...(policies === undefined ? {} : { policies }),
|
|
20
|
-
...(personas === undefined ? {} : { personas }),
|
|
21
21
|
...(defaults === undefined ? {} : { defaults }),
|
|
22
22
|
...(review === undefined ? {} : { review }),
|
|
23
23
|
...(config.execution?.desktop === undefined && config.execution?.target === undefined
|
|
@@ -43,8 +43,8 @@ function residualOf(config) {
|
|
|
43
43
|
export function isNonEmpty(values) {
|
|
44
44
|
return values.length > 0;
|
|
45
45
|
}
|
|
46
|
-
export function
|
|
47
|
-
const caps = config
|
|
46
|
+
export function planCaps(config) {
|
|
47
|
+
const caps = capsOf(config);
|
|
48
48
|
return {
|
|
49
49
|
...(caps?.maxUsd === undefined ? {} : { maxUsd: caps.maxUsd }),
|
|
50
50
|
...(caps?.maxTotalUsd === undefined ? {} : { maxTotalUsd: caps.maxTotalUsd }),
|
|
@@ -86,7 +86,7 @@ export function desktopRequirements(config, args) {
|
|
|
86
86
|
return requirements;
|
|
87
87
|
}
|
|
88
88
|
export function brainOf(config, callerProvider) {
|
|
89
|
-
const actor = config
|
|
89
|
+
const actor = actorOf(config);
|
|
90
90
|
if (callerProvider)
|
|
91
91
|
return callerBrainOf(config);
|
|
92
92
|
const declared = declaredModelOf(config);
|
|
@@ -99,7 +99,7 @@ export function callerBrainOf(config) {
|
|
|
99
99
|
return { kind: "caller", ...declaredModelOf(config) };
|
|
100
100
|
}
|
|
101
101
|
function declaredModelOf(config) {
|
|
102
|
-
const model = config
|
|
102
|
+
const model = actorOf(config)?.model;
|
|
103
103
|
return model === undefined ? {} : { declaredModel: model };
|
|
104
104
|
}
|
|
105
105
|
/** The model a participant's spend is priced at when its provider reports none. */
|
|
@@ -9,7 +9,7 @@ export interface Participant {
|
|
|
9
9
|
readonly id: string;
|
|
10
10
|
/** 0-based position in the roster. Bundle `sim-NNN` and `stream-NNN` ids derive from it. */
|
|
11
11
|
readonly index: number;
|
|
12
|
-
/** `
|
|
12
|
+
/** `participants[i].persona ?? actor.persona`. The route compiles the committed persona file. */
|
|
13
13
|
readonly personaId: string | undefined;
|
|
14
14
|
/** The declared mission and per-participant focus. An absent mission takes the route's default. */
|
|
15
15
|
readonly assignment: {
|
|
@@ -30,7 +30,7 @@ interface DesktopParticipant extends Participant {
|
|
|
30
30
|
readonly preset: DevicePreset;
|
|
31
31
|
readonly resolution: [number, number];
|
|
32
32
|
};
|
|
33
|
-
/** The `
|
|
33
|
+
/** The `participants` entry's value, else actor value, else absent (the provider default is recorded in the trace). */
|
|
34
34
|
readonly limits: {
|
|
35
35
|
readonly stopWhen?: StopWhen;
|
|
36
36
|
readonly dwell?: DwellWindow;
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// Computer-use and shared-world participants share the desktop fields; each kind adds its own.
|
|
3
3
|
import { resolveParticipantDevice } from "./device-presets.js";
|
|
4
4
|
import { isSharedWorldComposition, participantIdAt } from "./routing.js";
|
|
5
|
-
import {
|
|
5
|
+
import { actorOf, participantList, declaredParticipantCount, participantInstruction, } from "./study-fields.js";
|
|
6
6
|
function desktopParticipant(config, entry, index, kind, focus) {
|
|
7
|
-
const actor = config
|
|
7
|
+
const actor = actorOf(config);
|
|
8
8
|
const device = resolveParticipantDevice(config, entry?.device);
|
|
9
9
|
const personaId = entry?.persona ?? actor?.persona;
|
|
10
10
|
const mission = actor?.mission;
|
|
@@ -39,12 +39,14 @@ function desktopParticipant(config, entry, index, kind, focus) {
|
|
|
39
39
|
* override (`--count`) applies only when no roster is declared.
|
|
40
40
|
*/
|
|
41
41
|
export function computerUseParticipants(config, countOverride) {
|
|
42
|
-
const actor = config
|
|
43
|
-
const roster =
|
|
44
|
-
const count = roster
|
|
42
|
+
const actor = actorOf(config);
|
|
43
|
+
const roster = participantList(config);
|
|
44
|
+
const count = roster
|
|
45
|
+
? roster.length
|
|
46
|
+
: Math.max(1, countOverride ?? declaredParticipantCount(config) ?? 1);
|
|
45
47
|
return Array.from({ length: count }, (_, index) => {
|
|
46
48
|
const entry = roster?.[index];
|
|
47
|
-
const focus = roster ? entry?.instruction :
|
|
49
|
+
const focus = roster ? entry?.instruction : participantInstruction(config);
|
|
48
50
|
return {
|
|
49
51
|
...desktopParticipant(config, entry, index, "lane", focus),
|
|
50
52
|
...(actor?.tasks === undefined ? {} : { tasks: actor.tasks }),
|
|
@@ -54,7 +56,7 @@ export function computerUseParticipants(config, countOverride) {
|
|
|
54
56
|
}
|
|
55
57
|
/** The participants of a shared-world study, one per roster entry, typed by the plane they share. */
|
|
56
58
|
export function sharedWorldParticipants(config) {
|
|
57
|
-
const roster =
|
|
59
|
+
const roster = participantList(config) ?? [];
|
|
58
60
|
const participantAt = (entry, index) => desktopParticipant(config, entry, index, "seat", entry.instruction);
|
|
59
61
|
if (config.subject.source === "app-url") {
|
|
60
62
|
return {
|
|
@@ -75,18 +77,17 @@ export function sharedWorldParticipants(config) {
|
|
|
75
77
|
}
|
|
76
78
|
/** The ids of the participants a computer-use or shared-world study runs, in roster order. */
|
|
77
79
|
export function declaredParticipantIds(config) {
|
|
78
|
-
const
|
|
79
|
-
const roster = rosterOf(actor);
|
|
80
|
+
const roster = participantList(config);
|
|
80
81
|
const kind = isSharedWorldComposition(config) ? "seat" : "lane";
|
|
81
82
|
if (roster && roster.length > 0) {
|
|
82
83
|
return roster.map((entry, index) => participantIdAt(index, entry.id, kind));
|
|
83
84
|
}
|
|
84
|
-
const count = Math.max(1,
|
|
85
|
+
const count = Math.max(1, declaredParticipantCount(config) ?? 1);
|
|
85
86
|
return Array.from({ length: count }, (_, index) => participantIdAt(index, undefined, kind));
|
|
86
87
|
}
|
|
87
88
|
/** The entry URLs roster entries declare in place of the subject URL (computer use, app-url). */
|
|
88
89
|
export function declaredTargets(config) {
|
|
89
|
-
return (
|
|
90
|
+
return (participantList(config) ?? [])
|
|
90
91
|
.map((entry) => entry.target)
|
|
91
92
|
.filter((target) => target !== undefined);
|
|
92
93
|
}
|
|
@@ -8,7 +8,7 @@ import type { RunStudyProvenance } from "../run/study-provenance.js";
|
|
|
8
8
|
import type { ComputerUseRefusal } from "../routes/computer-use/plan.js";
|
|
9
9
|
import type { SharedWorldRefusal } from "../routes/shared-world/plan.js";
|
|
10
10
|
import type { ComputerUseParticipant, ExternalPublicParticipant, ProvisionedParticipant } from "./plan-participants.js";
|
|
11
|
-
import type { StudyConfig, StudyExecutionTerminal, StudyRuntimeAuth,
|
|
11
|
+
import type { StudyConfig, StudyExecutionTerminal, StudyRuntimeAuth, StudyCaps, StudySubjectProduct, StudySubjectServe, StudySubjectState, StudySubjectStateCheckpoint } from "./types.js";
|
|
12
12
|
export type NonEmpty<T> = readonly [T, ...T[]];
|
|
13
13
|
type AtLeastTwo<T> = readonly [T, T, ...T[]];
|
|
14
14
|
/**
|
|
@@ -48,7 +48,7 @@ export type Requirement = {
|
|
|
48
48
|
* VM study read it before a plan exists. Shared world, terminal and scripted still read some config
|
|
49
49
|
* fields at run time.
|
|
50
50
|
*/
|
|
51
|
-
export type ResidualConfig = Pick<StudyConfig, "comms" | "policies" | "
|
|
51
|
+
export type ResidualConfig = Pick<StudyConfig, "comms" | "policies" | "defaults" | "review"> & {
|
|
52
52
|
readonly execution?: Pick<NonNullable<StudyConfig["execution"]>, "desktop" | "target">;
|
|
53
53
|
readonly subject: Pick<StudyConfig["subject"], "clone" | "localTree" | "repos" | "envValues">;
|
|
54
54
|
};
|
|
@@ -83,7 +83,7 @@ interface PreviewPlan extends PlanBase {
|
|
|
83
83
|
}
|
|
84
84
|
/**
|
|
85
85
|
* The model driving a desktop participant. `caller` is the library caller's createProvider.
|
|
86
|
-
* `declaredModel` is
|
|
86
|
+
* `declaredModel` is actor.model as the study wrote it, absent when undeclared: the providers
|
|
87
87
|
* take it as written, and spend is priced at it, else the default (pricedModel). An openai
|
|
88
88
|
* brain's `model` is the one its provider runs, with that default applied.
|
|
89
89
|
*/
|
|
@@ -229,10 +229,10 @@ export type TerminalPlan = PlanBase & {
|
|
|
229
229
|
readonly stdin?: NonNullable<StudyExecutionTerminal["stdin"]>;
|
|
230
230
|
} & ({
|
|
231
231
|
readonly dryRun: true;
|
|
232
|
-
readonly caps?:
|
|
232
|
+
readonly caps?: StudyCaps;
|
|
233
233
|
} | {
|
|
234
234
|
readonly dryRun: false;
|
|
235
|
-
readonly caps:
|
|
235
|
+
readonly caps: StudyCaps & {
|
|
236
236
|
readonly maxUsd: number;
|
|
237
237
|
readonly maxMinutes: number;
|
|
238
238
|
};
|
package/dist/study/plan.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The route decision. A study's route follows from its composition (subject.source,
|
|
2
|
-
// execution.target, the
|
|
2
|
+
// execution.target, the actor's registered run kind, `route: shared-world`), never from a
|
|
3
3
|
// declared kind. This is the only function that decides it.
|
|
4
4
|
import { resolveAutomaticAnalysis } from "../analysis/automatic-config.js";
|
|
5
5
|
import { callerDrivingOf, planComputerUseStudy } from "../routes/computer-use/plan.js";
|
|
@@ -11,16 +11,17 @@ import { THIS_REPO_DRY_RUN_ONLY } from "./composition-rules.js";
|
|
|
11
11
|
import { planBase } from "./plan-base.js";
|
|
12
12
|
import { routeOf } from "./routing.js";
|
|
13
13
|
import { automaticAnalysisRouteReason, taskProtocolValidationReason } from "./validation.js";
|
|
14
|
+
import { declaredParticipantCount, modeOf } from "./study-fields.js";
|
|
14
15
|
export { routeOf } from "./routing.js";
|
|
15
16
|
/** Resolve dry-run: explicit override wins, else the scenario mode, else the given fallback. */
|
|
16
17
|
export function resolveStudyDryRun(config, override, fallback) {
|
|
17
18
|
if (override !== undefined) {
|
|
18
19
|
return override;
|
|
19
20
|
}
|
|
20
|
-
if (config
|
|
21
|
+
if (modeOf(config) === "live") {
|
|
21
22
|
return false;
|
|
22
23
|
}
|
|
23
|
-
if (config
|
|
24
|
+
if (modeOf(config) === "dry-run") {
|
|
24
25
|
return true;
|
|
25
26
|
}
|
|
26
27
|
return fallback;
|
|
@@ -45,7 +46,7 @@ function planPreview(config, options, input) {
|
|
|
45
46
|
const tasksReason = taskProtocolValidationReason(config);
|
|
46
47
|
if (tasksReason)
|
|
47
48
|
return refuse("HUMANISH_STUDY_TASKS_UNSUPPORTED", tasksReason);
|
|
48
|
-
const participantCount = options.count ?? config
|
|
49
|
+
const participantCount = options.count ?? declaredParticipantCount(config) ?? 4;
|
|
49
50
|
if (!Number.isSafeInteger(participantCount) || participantCount < 1)
|
|
50
51
|
return refuse("HUMANISH_INVALID_PARTICIPANT_COUNT", "count must be a positive integer.");
|
|
51
52
|
if (!input.dryRun)
|
|
@@ -12,9 +12,9 @@ import { acquireE2BDesktopSandbox, readE2BRelease } from "../substrates/e2b/sand
|
|
|
12
12
|
import { e2bShell } from "../substrates/e2b/shell.js";
|
|
13
13
|
import { redactText } from "../evidence/redaction.js";
|
|
14
14
|
import { digest, fail, finalize } from "./preflight-result.js";
|
|
15
|
-
import { rosterOf } from "./parse/actors.js";
|
|
16
15
|
import { plural } from "../run/text.js";
|
|
17
16
|
import { cli } from "../cli/invocation.js";
|
|
17
|
+
import { participantList } from "./study-fields.js";
|
|
18
18
|
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
|
19
19
|
// Room on the probe's lease for desktop boot and teardown around the work it does.
|
|
20
20
|
const PREFLIGHT_LEASE_BUFFER_MS = 5 * 60_000;
|
|
@@ -25,7 +25,7 @@ export async function runPublicPreviewPreflight(ctx) {
|
|
|
25
25
|
{ name: "route", ok: false, message: routeError },
|
|
26
26
|
]);
|
|
27
27
|
}
|
|
28
|
-
const rosterTargets = ctx.targets.filter((target) => target.kind === "
|
|
28
|
+
const rosterTargets = ctx.targets.filter((target) => target.kind === "participants[].target");
|
|
29
29
|
const publicTargets = rosterTargets.length > 0
|
|
30
30
|
? rosterTargets
|
|
31
31
|
: ctx.targets.filter((target) => target.kind === "subject.appUrl");
|
|
@@ -270,8 +270,8 @@ function targetUrlFor(config, target) {
|
|
|
270
270
|
if (target.kind === "subject.appUrl" && config.subject.appUrl) {
|
|
271
271
|
return config.subject.appUrl;
|
|
272
272
|
}
|
|
273
|
-
if (target.kind === "
|
|
274
|
-
const rosterTarget =
|
|
273
|
+
if (target.kind === "participants[].target") {
|
|
274
|
+
const rosterTarget = participantList(config)?.find((entry) => entry.target && digest(entry.target) === target.targetDigest)?.target;
|
|
275
275
|
if (rosterTarget)
|
|
276
276
|
return rosterTarget;
|
|
277
277
|
}
|
|
@@ -12,7 +12,7 @@ export interface StudyPreflightCheck {
|
|
|
12
12
|
}
|
|
13
13
|
export interface StudyPreflightTarget {
|
|
14
14
|
label: string;
|
|
15
|
-
kind: "subject.appUrl" | "
|
|
15
|
+
kind: "subject.appUrl" | "participants[].target" | "subject.serve.url" | "subject.product.publicSurface";
|
|
16
16
|
targetDigest: string;
|
|
17
17
|
originDigest?: string;
|
|
18
18
|
loopback: boolean;
|
package/dist/study/preflight.js
CHANGED
|
@@ -5,7 +5,7 @@ import { runPublicPreviewPreflight, runSandboxLoopbackPreflight } from "./prefli
|
|
|
5
5
|
import { digest, fail, finalize, STUDY_CHECK_SCHEMA } from "./preflight-result.js";
|
|
6
6
|
import { routeOf } from "./plan.js";
|
|
7
7
|
import { resolveStudyManifest } from "./discover.js";
|
|
8
|
-
import {
|
|
8
|
+
import { participantList } from "./study-fields.js";
|
|
9
9
|
const DEFAULT_PREFLIGHT_TIMEOUT_MS = 30_000;
|
|
10
10
|
export async function runStudyPreflight(options) {
|
|
11
11
|
const cwd = path.resolve(options.cwd);
|
|
@@ -82,9 +82,9 @@ function collectTargets(config) {
|
|
|
82
82
|
if (config.subject.appUrl) {
|
|
83
83
|
targets.push(makeTarget("subject.appUrl", "subject.appUrl", config.subject.appUrl));
|
|
84
84
|
}
|
|
85
|
-
for (const [index, entry] of (
|
|
85
|
+
for (const [index, entry] of (participantList(config) ?? []).entries()) {
|
|
86
86
|
if (entry.target) {
|
|
87
|
-
targets.push(makeTarget(`
|
|
87
|
+
targets.push(makeTarget(`participants[${index}].target`, "participants[].target", entry.target));
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
if (config.subject.serve?.url) {
|
package/dist/study/routing.d.ts
CHANGED
|
@@ -9,8 +9,8 @@ export declare function registeredScriptedBrowserActors(): string[];
|
|
|
9
9
|
export declare function actorResolvesToTerminal(type: string | undefined): boolean;
|
|
10
10
|
export declare function registeredTerminalActors(): string[];
|
|
11
11
|
/**
|
|
12
|
-
* The declared fan-out participant count on the computer-use route: a `
|
|
13
|
-
* a
|
|
12
|
+
* The declared fan-out participant count on the computer-use route: a `participants` list's
|
|
13
|
+
* length, else a participant count, else 1. The single source of truth shared by the parser, the engine,
|
|
14
14
|
* and the pre-flight plan so the participant count is computed the same way everywhere.
|
|
15
15
|
*/
|
|
16
16
|
export declare function computerUseParticipantCount(config: StudyConfig): number;
|
|
@@ -36,15 +36,15 @@ export declare function isComputerUseComposition(config: StudyConfig): boolean;
|
|
|
36
36
|
* `shared-world` topology. Mirror of isComputerUseComposition; the single source of truth shared by
|
|
37
37
|
* routeOf (which checks it before the computer-use route) and the warning logic. Every
|
|
38
38
|
* shared-world study runs its participants at once (`execution.concurrency` >= 2). The same
|
|
39
|
-
* clone/local-tree × e2b-desktop × computer-use composition without `
|
|
40
|
-
* (the computer-use route); the
|
|
39
|
+
* clone/local-tree × e2b-desktop × computer-use composition without `route: shared-world` stays per-lane-worlds
|
|
40
|
+
* (the computer-use route); the route declaration is the override switch.
|
|
41
41
|
*/
|
|
42
42
|
export declare function isSharedWorldComposition(config: StudyConfig): boolean;
|
|
43
43
|
/** The getHost provisioned-subject shared-world shape (clone/local-tree served + exposed in-sandbox). */
|
|
44
44
|
export declare function isProvisionedSharedWorldComposition(config: StudyConfig): boolean;
|
|
45
45
|
/**
|
|
46
46
|
* The external-public shared-world shape: a real public deployment used directly as
|
|
47
|
-
* the shared plane: `source: app-url` + `
|
|
47
|
+
* the shared plane: `source: app-url` + `route: shared-world` + a computer-use actor on
|
|
48
48
|
* e2b-desktop + `policies.allowPublicTargets: true`. No getHost, no clone, no subject sandbox, no
|
|
49
49
|
* seed. The operator-ownership attestation `subject.publicTarget` is required (validated in
|
|
50
50
|
* externalPublicSharedWorldValidationReason, not here; this predicate is the router only, so a
|