humanish 0.110.2 → 0.110.4
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/ARCHITECTURE.md +10 -15
- package/CHANGELOG.md +27 -0
- package/CONTEXT.md +1 -1
- package/dist/actors/computer-use/actor.js +2 -2
- package/dist/actors/computer-use/loop/ending.js +1 -1
- package/dist/actors/registry.js +1 -1
- 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 +15 -2
- package/dist/cli/commands/migrate.js +1 -1
- package/dist/cli/commands/observe.js +3 -2
- 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-run.js +9 -19
- package/dist/cli/commands/study.js +4 -1
- package/dist/cli/findings.d.ts +5 -0
- package/dist/cli/findings.js +21 -0
- package/dist/cli/io.d.ts +2 -1
- package/dist/cli/io.js +3 -2
- package/dist/cli/observer-follow.d.ts +6 -2
- package/dist/cli/observer-follow.js +15 -5
- package/dist/evidence/redaction.js +15 -4
- 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-runs.d.ts +6 -9
- package/dist/routes/computer-use/participant-runs.js +16 -36
- package/dist/routes/computer-use/plan.d.ts +2 -2
- package/dist/routes/computer-use/plan.js +7 -6
- package/dist/routes/computer-use/route.d.ts +5 -18
- package/dist/routes/computer-use/route.js +19 -79
- package/dist/routes/computer-use/setup.d.ts +3 -2
- package/dist/routes/computer-use/setup.js +2 -1
- package/dist/routes/computer-use/types.d.ts +6 -10
- package/dist/routes/scripted/plan.js +5 -4
- package/dist/routes/scripted/route.d.ts +4 -17
- package/dist/routes/scripted/route.js +15 -43
- 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 +1 -6
- package/dist/routes/shared-world/participant-specs.js +5 -1
- package/dist/routes/shared-world/plan.js +7 -6
- package/dist/routes/shared-world/route.d.ts +5 -11
- package/dist/routes/shared-world/route.js +27 -66
- package/dist/routes/shared-world/types.d.ts +2 -6
- package/dist/routes/terminal/plan.js +4 -3
- package/dist/routes/terminal/route.d.ts +3 -13
- package/dist/routes/terminal/route.js +21 -60
- package/dist/routes/terminal/session.d.ts +2 -3
- package/dist/routes/terminal/session.js +3 -3
- package/dist/routes/terminal/types.d.ts +2 -7
- 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/route-shell.d.ts +60 -0
- package/dist/run/route-shell.js +51 -0
- package/dist/run/run.d.ts +16 -1
- package/dist/run/run.js +33 -5
- package/dist/run/status.d.ts +8 -0
- package/dist/run/status.js +6 -0
- package/dist/run/stored-runs.js +26 -11
- package/dist/study/composition-rules.js +33 -33
- package/dist/study/config.d.ts +9 -2
- package/dist/study/config.js +14 -5
- package/dist/study/doctor.js +5 -4
- package/dist/study/init-templates.d.ts +1 -1
- package/dist/study/init-templates.js +43 -34
- package/dist/study/init.js +1 -1
- package/dist/study/{convert.d.ts → migrate/convert.d.ts} +1 -1
- package/dist/study/{convert.js → migrate/convert.js} +29 -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 +68 -0
- package/dist/study/migrate/v2.js +480 -0
- package/dist/study/parse/actors.d.ts +46 -1
- package/dist/study/parse/actors.js +116 -69
- package/dist/study/parse/execution.js +2 -2
- package/dist/study/parse/study-v3.d.ts +26 -0
- package/dist/study/parse/study-v3.js +53 -35
- package/dist/study/parse/study.d.ts +38 -0
- package/dist/study/parse/study.js +225 -0
- package/dist/study/parse/subject.js +1 -1
- package/dist/study/persona-resolve.d.ts +2 -4
- package/dist/study/persona-resolve.js +2 -1
- package/dist/study/plan-base.d.ts +1 -1
- package/dist/study/plan-base.js +5 -4
- package/dist/study/plan-participants.js +12 -11
- package/dist/study/plan.js +4 -3
- package/dist/study/preflight-probes.js +2 -2
- package/dist/study/preflight.js +2 -2
- package/dist/study/routing.js +12 -15
- package/dist/study/run-study-options.js +3 -3
- package/dist/study/study-fields.d.ts +28 -0
- package/dist/study/study-fields.js +51 -0
- package/dist/study/summary.js +13 -17
- package/dist/study/types.d.ts +40 -0
- package/dist/study/url-credentials.js +2 -1
- package/dist/study/validation.d.ts +2 -0
- package/dist/study/validation.js +46 -41
- package/dist/study/warnings.js +5 -4
- package/dist/substrates/local/runtime-config.js +9 -9
- package/dist/tui-app.js +3 -3
- package/docs/architecture/actor-contract.md +1 -1
- package/docs/architecture/external-public-shared-world.md +1 -1
- package/docs/architecture/terminal-product-route.md +13 -13
- package/docs/contracts/schemas.md +13 -4
- package/docs/contracts/study-analysis.md +5 -2
- package/docs/product/automatic-analysis.md +1 -1
- package/package.json +1 -1
- /package/dist/study/{owned-files.d.ts → migrate/owned-files.d.ts} +0 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// humanish.study.v3, parsed into a config with the file's own keys (StudyV3). parseStudy still
|
|
2
|
+
// rewrites a v3 file into the v2 spelling and parses that (parse/study-v3.ts).
|
|
3
|
+
// tests/study/parse-differential.test.ts holds the two to the same result. The checks that read
|
|
4
|
+
// more than one section still take the v2 shape, so they run on toLegacy's view of the study.
|
|
5
|
+
import { isRecord } from "../../run/type-guards.js";
|
|
6
|
+
import { checkStudyConfig } from "../config.js";
|
|
7
|
+
import { routeOf } from "../routing.js";
|
|
8
|
+
import { ID_PATTERN, STUDY_SCHEMA, V2_SCHEMA, } from "../types.js";
|
|
9
|
+
import { inertFieldLabels, inertFieldPaths } from "../warnings.js";
|
|
10
|
+
import { parseActorFields, parseActorType, parseParticipantEntries, withoutParticipants, withParticipants, } from "./actors.js";
|
|
11
|
+
import { parseComms } from "./comms.js";
|
|
12
|
+
import { parseDefaults, parseExecution, parsePolicies, parseReview } from "./execution.js";
|
|
13
|
+
import { readStudyFront, studySpelling } from "./study-v3.js";
|
|
14
|
+
import { parseSubject } from "./subject.js";
|
|
15
|
+
import { invalid, nonNegNumber, optionalStr, str } from "./values.js";
|
|
16
|
+
/**
|
|
17
|
+
* Validate a parsed humanish.study.v3 document into a StudyV3, with the messages parseStudy gives.
|
|
18
|
+
* Pure: the caller owns file IO. parseStudy refuses a humanish.lab.v2 document before this runs.
|
|
19
|
+
*/
|
|
20
|
+
export function parseStudyV3(raw, options = {}) {
|
|
21
|
+
if (!isRecord(raw))
|
|
22
|
+
return invalid("A study file must be a YAML object.");
|
|
23
|
+
if (raw.schema !== STUDY_SCHEMA) {
|
|
24
|
+
return invalid(`The study schema must be ${STUDY_SCHEMA} or ${V2_SCHEMA}.`);
|
|
25
|
+
}
|
|
26
|
+
const front = readStudyFront(raw, options.inert === "report");
|
|
27
|
+
if (!front.ok)
|
|
28
|
+
return front;
|
|
29
|
+
const study = parseSections(raw, front.value);
|
|
30
|
+
if (!study.ok)
|
|
31
|
+
return study;
|
|
32
|
+
const spell = (message) => options.inert === "report"
|
|
33
|
+
? message
|
|
34
|
+
: studySpelling(message, front.value.route, front.value.participants.source);
|
|
35
|
+
const checked = checkStudyConfig(toLegacy(study.value));
|
|
36
|
+
if (!checked.ok)
|
|
37
|
+
return invalid(spell(checked.error.message));
|
|
38
|
+
const { route } = study.value;
|
|
39
|
+
const taken = routeOf(checked.config);
|
|
40
|
+
if (taken !== route) {
|
|
41
|
+
return invalid(`This study declares route: ${route}, but its subject (source: ${checked.config.subject.source}) and actor (type: ${checked.config.actors[0]?.type}) take the ${taken} route. Change \`route\`, or change the subject or actor.`);
|
|
42
|
+
}
|
|
43
|
+
const config = withFilledDefaults(study.value, checked.config);
|
|
44
|
+
const warnings = checked.warnings.map(spell);
|
|
45
|
+
if (options.inert === "report") {
|
|
46
|
+
return { ok: true, config, warnings, inert: inertFieldPaths(checked.config) };
|
|
47
|
+
}
|
|
48
|
+
const inert = inertFieldLabels(checked.config);
|
|
49
|
+
if (inert.length > 0) {
|
|
50
|
+
return invalid(spell(`route: ${route} does not read ${inert.join(", ")}. Remove ${inert.length === 1 ? "it" : "them"}.`));
|
|
51
|
+
}
|
|
52
|
+
return { ok: true, config, warnings };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* A v3 study in the v2 spelling: the actor holds its participants, `caps` moves under `execution`
|
|
56
|
+
* or `scenario`, and a shared world is `subject.topology: shared-world`. The checks that read more
|
|
57
|
+
* than one section run on this view until StudyConfig has the v3 shape.
|
|
58
|
+
*/
|
|
59
|
+
export function toLegacy(study) {
|
|
60
|
+
const { route, participants, caps } = study;
|
|
61
|
+
const group = typeof participants === "object" && !Array.isArray(participants) ? participants : undefined;
|
|
62
|
+
const actor = withParticipants(study.actor, {
|
|
63
|
+
count: typeof participants === "number" ? participants : (group?.count ?? study.surfaces?.length),
|
|
64
|
+
entries: Array.isArray(participants) ? participants : undefined,
|
|
65
|
+
instruction: group?.instruction,
|
|
66
|
+
});
|
|
67
|
+
const { source, ...subject } = study.subject;
|
|
68
|
+
const execution = caps !== undefined && route !== "terminal" ? { ...study.execution, caps } : study.execution;
|
|
69
|
+
const scenario = {
|
|
70
|
+
...(study.scenario === undefined ? {} : { ref: study.scenario }),
|
|
71
|
+
...(study.mode === undefined ? {} : { mode: study.mode }),
|
|
72
|
+
...(caps !== undefined && route === "terminal" ? { caps } : {}),
|
|
73
|
+
};
|
|
74
|
+
return {
|
|
75
|
+
schema: study.schema,
|
|
76
|
+
id: study.id,
|
|
77
|
+
...(study.title === undefined ? {} : { title: study.title }),
|
|
78
|
+
...(study.description === undefined ? {} : { description: study.description }),
|
|
79
|
+
subject: route === "shared-world" ? { source, topology: "shared-world", ...subject } : study.subject,
|
|
80
|
+
actors: [actor],
|
|
81
|
+
...(execution === undefined ? {} : { execution }),
|
|
82
|
+
...(Object.keys(scenario).length > 0 ? { scenario } : {}),
|
|
83
|
+
...(study.policies === undefined ? {} : { policies: study.policies }),
|
|
84
|
+
...(study.review === undefined ? {} : { review: study.review }),
|
|
85
|
+
...(study.defaults === undefined ? {} : { defaults: study.defaults }),
|
|
86
|
+
...(study.comms === undefined ? {} : { comms: study.comms }),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
// Each section in the order the v2 parser reads it, so a file with more than one error reports the
|
|
90
|
+
// same one first.
|
|
91
|
+
function parseSections(raw, front) {
|
|
92
|
+
const id = str(raw.id);
|
|
93
|
+
if (!id || !ID_PATTERN.test(id)) {
|
|
94
|
+
return invalid("The study id must be a public-safe token starting with a letter or digit (/^[A-Za-z0-9][A-Za-z0-9_.-]*$/).");
|
|
95
|
+
}
|
|
96
|
+
const subject = parseSubject(raw.subject);
|
|
97
|
+
if (!subject.ok)
|
|
98
|
+
return subject;
|
|
99
|
+
const type = parseActorType(front.actor, "actor");
|
|
100
|
+
if (!type.ok)
|
|
101
|
+
return type;
|
|
102
|
+
const participants = parseParticipants(raw.participants, front.participants);
|
|
103
|
+
if (!participants.ok)
|
|
104
|
+
return participants;
|
|
105
|
+
const fields = parseActorFields(front.actor, "actor");
|
|
106
|
+
if (!fields.ok)
|
|
107
|
+
return fields;
|
|
108
|
+
const execution = parseExecution(raw.execution);
|
|
109
|
+
if (!execution.ok)
|
|
110
|
+
return execution;
|
|
111
|
+
const caps = parseCaps(raw.caps);
|
|
112
|
+
if (!caps.ok)
|
|
113
|
+
return caps;
|
|
114
|
+
if (isRecord(raw.policies) &&
|
|
115
|
+
raw.policies.mediaPermission !== undefined &&
|
|
116
|
+
raw.policies.mediaPermission !== "prompt" &&
|
|
117
|
+
raw.policies.mediaPermission !== "granted") {
|
|
118
|
+
return invalid("`policies.mediaPermission` must be `prompt` (the participant answers the browser's own dialog) or `granted`.");
|
|
119
|
+
}
|
|
120
|
+
const policies = parsePolicies(raw.policies);
|
|
121
|
+
if (!policies.ok)
|
|
122
|
+
return policies;
|
|
123
|
+
const review = parseReview(raw.review);
|
|
124
|
+
if (!review.ok)
|
|
125
|
+
return review;
|
|
126
|
+
if (isRecord(raw.defaults) && raw.defaults.open !== undefined) {
|
|
127
|
+
if (typeof raw.defaults.open !== "boolean")
|
|
128
|
+
return invalid("`defaults.open` must be true or false.");
|
|
129
|
+
}
|
|
130
|
+
const defaults = parseDefaults(raw.defaults);
|
|
131
|
+
const comms = parseComms(raw.comms);
|
|
132
|
+
if (!comms.ok)
|
|
133
|
+
return comms;
|
|
134
|
+
const mode = raw.mode === "dry-run" || raw.mode === "live" ? raw.mode : undefined;
|
|
135
|
+
const surfaces = front.surfaces === 2
|
|
136
|
+
? ["desktop", "mobile"]
|
|
137
|
+
: front.surfaces === 1
|
|
138
|
+
? ["desktop"]
|
|
139
|
+
: undefined;
|
|
140
|
+
const scenario = str(raw.scenario);
|
|
141
|
+
return {
|
|
142
|
+
ok: true,
|
|
143
|
+
value: {
|
|
144
|
+
schema: STUDY_SCHEMA,
|
|
145
|
+
id,
|
|
146
|
+
...optionalStr("title", raw.title),
|
|
147
|
+
...optionalStr("description", raw.description),
|
|
148
|
+
route: front.route,
|
|
149
|
+
...(mode === undefined ? {} : { mode }),
|
|
150
|
+
subject: subject.value,
|
|
151
|
+
actor: { type: type.value, ...fields.value },
|
|
152
|
+
...(participants.value === undefined ? {} : { participants: participants.value }),
|
|
153
|
+
...(surfaces === undefined ? {} : { surfaces }),
|
|
154
|
+
...(caps.value === undefined ? {} : { caps: caps.value }),
|
|
155
|
+
...(execution.value === undefined ? {} : { execution: execution.value }),
|
|
156
|
+
...(scenario === undefined ? {} : { scenario }),
|
|
157
|
+
...(policies.value === undefined ? {} : { policies: policies.value }),
|
|
158
|
+
...(review.value === undefined ? {} : { review: review.value }),
|
|
159
|
+
...(defaults === undefined ? {} : { defaults }),
|
|
160
|
+
...(comms.value === undefined ? {} : { comms: comms.value }),
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
// `participants` in the form the file wrote it. readStudyFront has checked the form against the
|
|
165
|
+
// route and expanded each list entry that has a count; the entries parse here, each named by the
|
|
166
|
+
// `participants` entry it came from. An instruction that is not a non-empty string is dropped, as
|
|
167
|
+
// the v2 parser drops it.
|
|
168
|
+
function parseParticipants(raw, read) {
|
|
169
|
+
if (read.entries !== undefined) {
|
|
170
|
+
const { source } = read;
|
|
171
|
+
return parseParticipantEntries(read.entries, {
|
|
172
|
+
list: "participants",
|
|
173
|
+
entry: (index) => `participants[${source[index] ?? index}]`,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
if (typeof raw === "number")
|
|
177
|
+
return { ok: true, value: read.count };
|
|
178
|
+
if (!isRecord(raw))
|
|
179
|
+
return { ok: true, value: undefined };
|
|
180
|
+
const instruction = str(read.instruction);
|
|
181
|
+
return {
|
|
182
|
+
ok: true,
|
|
183
|
+
value: {
|
|
184
|
+
...(read.count === undefined ? {} : { count: read.count }),
|
|
185
|
+
...(instruction === undefined ? {} : { instruction }),
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* A caps block's values. A value that is not a non-negative number refuses the study: a cap that
|
|
191
|
+
* does nothing must not look like one that holds. readStudyFront has checked the keys against the
|
|
192
|
+
* route; migrate checks a v2 file's two blocks with it.
|
|
193
|
+
*/
|
|
194
|
+
export function parseCaps(raw) {
|
|
195
|
+
if (raw === undefined)
|
|
196
|
+
return { ok: true, value: undefined };
|
|
197
|
+
if (!isRecord(raw)) {
|
|
198
|
+
return invalid("`caps` must be an object ({ maxUsd?, maxTotalUsd?, maxJobs?, maxMinutes? }).");
|
|
199
|
+
}
|
|
200
|
+
const caps = {};
|
|
201
|
+
for (const key of ["maxUsd", "maxTotalUsd", "maxJobs", "maxMinutes"]) {
|
|
202
|
+
if (raw[key] === undefined)
|
|
203
|
+
continue;
|
|
204
|
+
const value = nonNegNumber(raw[key]);
|
|
205
|
+
if (value === undefined)
|
|
206
|
+
return invalid(`\`caps.${key}\` must be a non-negative number.`);
|
|
207
|
+
caps[key] = value;
|
|
208
|
+
}
|
|
209
|
+
return { ok: true, value: Object.keys(caps).length > 0 ? caps : undefined };
|
|
210
|
+
}
|
|
211
|
+
// checkStudyConfig fills defaults in the v2 view: a shared world's `execution.concurrency`,
|
|
212
|
+
// `comms.email.recipients`, and the local browser's actor model and effort, execution timeout and
|
|
213
|
+
// resolution and `review.analysis`. Each has the same path in both shapes, so it is copied back.
|
|
214
|
+
function withFilledDefaults(study, checked) {
|
|
215
|
+
const [actor] = checked.actors;
|
|
216
|
+
const execution = { ...checked.execution };
|
|
217
|
+
delete execution.caps;
|
|
218
|
+
return {
|
|
219
|
+
...study,
|
|
220
|
+
actor: actor === undefined ? study.actor : withoutParticipants(actor),
|
|
221
|
+
...(Object.keys(execution).length > 0 ? { execution } : {}),
|
|
222
|
+
...(checked.review === undefined ? {} : { review: checked.review }),
|
|
223
|
+
...(checked.comms === undefined ? {} : { comms: checked.comms }),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
@@ -176,7 +176,7 @@ function parseServedApp(raw, subject) {
|
|
|
176
176
|
return stateResult;
|
|
177
177
|
}
|
|
178
178
|
if (stateResult.value) {
|
|
179
|
-
// Semantic validation is shared with the engine (
|
|
179
|
+
// Semantic validation is shared with the engine (planComputerUseStudy re-enforces it for
|
|
180
180
|
// configs that arrive through the library API without the parser).
|
|
181
181
|
const reason = subjectStateInvalidReason(stateResult.value, subject.env);
|
|
182
182
|
if (reason) {
|
|
@@ -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
|
|
@@ -6,6 +6,7 @@ import { prepareSelectedOutputDirectory, readContainedRegularFile, } from "../ru
|
|
|
6
6
|
import { digestText, redactText } from "../evidence/redaction.js";
|
|
7
7
|
import { realpath } from "node:fs/promises";
|
|
8
8
|
import { rosterOf } from "./parse/actors.js";
|
|
9
|
+
import { actorsOf } from "./study-fields.js";
|
|
9
10
|
/** Persona ids are file-name segments, never paths: the same grammar the terminal route enforces. */
|
|
10
11
|
const PERSONA_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
11
12
|
/** Title-case an id for the fallback display name (`skeptical-power-user` -> `Skeptical Power User`). */
|
|
@@ -86,7 +87,7 @@ export async function resolveCommittedPersonas(projectRoot, personaIds) {
|
|
|
86
87
|
*/
|
|
87
88
|
export function studyPersonaIds(config) {
|
|
88
89
|
const ids = [];
|
|
89
|
-
for (const actor of config
|
|
90
|
+
for (const actor of actorsOf(config)) {
|
|
90
91
|
if (actor.persona)
|
|
91
92
|
ids.push(actor.persona);
|
|
92
93
|
for (const entry of rosterOf(actor) ?? []) {
|
|
@@ -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, route: "computer-use" | "shared-world"): 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()) {
|
|
@@ -43,8 +44,8 @@ function residualOf(config) {
|
|
|
43
44
|
export function isNonEmpty(values) {
|
|
44
45
|
return values.length > 0;
|
|
45
46
|
}
|
|
46
|
-
export function
|
|
47
|
-
const caps = config
|
|
47
|
+
export function planCaps(config, route) {
|
|
48
|
+
const caps = capsOf(config, route);
|
|
48
49
|
return {
|
|
49
50
|
...(caps?.maxUsd === undefined ? {} : { maxUsd: caps.maxUsd }),
|
|
50
51
|
...(caps?.maxTotalUsd === undefined ? {} : { maxTotalUsd: caps.maxTotalUsd }),
|
|
@@ -86,7 +87,7 @@ export function desktopRequirements(config, args) {
|
|
|
86
87
|
return requirements;
|
|
87
88
|
}
|
|
88
89
|
export function brainOf(config, callerProvider) {
|
|
89
|
-
const actor = config
|
|
90
|
+
const actor = actorOf(config);
|
|
90
91
|
if (callerProvider)
|
|
91
92
|
return callerBrainOf(config);
|
|
92
93
|
const declared = declaredModelOf(config);
|
|
@@ -99,7 +100,7 @@ export function callerBrainOf(config) {
|
|
|
99
100
|
return { kind: "caller", ...declaredModelOf(config) };
|
|
100
101
|
}
|
|
101
102
|
function declaredModelOf(config) {
|
|
102
|
-
const model = config
|
|
103
|
+
const model = actorOf(config)?.model;
|
|
103
104
|
return model === undefined ? {} : { declaredModel: model };
|
|
104
105
|
}
|
|
105
106
|
/** The model a participant's spend is priced at when its provider reports none. */
|
|
@@ -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
|
}
|
package/dist/study/plan.js
CHANGED
|
@@ -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;
|
|
@@ -271,7 +271,7 @@ function targetUrlFor(config, target) {
|
|
|
271
271
|
return config.subject.appUrl;
|
|
272
272
|
}
|
|
273
273
|
if (target.kind === "actors[0].lanes[].target") {
|
|
274
|
-
const rosterTarget =
|
|
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
|
}
|
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,7 +82,7 @@ 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
87
|
targets.push(makeTarget(`actors[0].lanes[${index}].target`, "actors[0].lanes[].target", entry.target));
|
|
88
88
|
}
|
package/dist/study/routing.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { actorRegistry } from "../actors/registry.js";
|
|
2
2
|
import { isLoopbackUrl } from "./parse/subject.js";
|
|
3
|
+
import { actorOf, participantList, declaredParticipantCount, declaresSharedWorld, } from "./study-fields.js";
|
|
3
4
|
// Hard cap on computer-use participants. No setting raises it: each participant is a paid desktop,
|
|
4
5
|
// and they all run at once.
|
|
5
6
|
export const MAX_COMPUTER_USE_PARTICIPANTS = 16;
|
|
@@ -50,11 +51,7 @@ export function registeredTerminalActors() {
|
|
|
50
51
|
* and the pre-flight plan so the participant count is computed the same way everywhere.
|
|
51
52
|
*/
|
|
52
53
|
export function computerUseParticipantCount(config) {
|
|
53
|
-
|
|
54
|
-
if (actor?.lanes !== undefined) {
|
|
55
|
-
return actor.lanes.length;
|
|
56
|
-
}
|
|
57
|
-
return actor?.count ?? 1;
|
|
54
|
+
return participantList(config)?.length ?? declaredParticipantCount(config) ?? 1;
|
|
58
55
|
}
|
|
59
56
|
/**
|
|
60
57
|
* A participant's id: its declared roster id, else `lane-NN` (independent participants) or `role-NN`
|
|
@@ -77,7 +74,7 @@ export function isComputerUseComposition(config) {
|
|
|
77
74
|
// local-app drives the cua loop in-process (a custom executor + a non-vision provider), so it
|
|
78
75
|
// routes to the computer-use route exactly like an app-url subject with a computer-use actor.
|
|
79
76
|
if (config.subject.source === "app-url" || config.subject.source === "local-app") {
|
|
80
|
-
return actorResolvesToComputerUse(config
|
|
77
|
+
return actorResolvesToComputerUse(actorOf(config)?.type);
|
|
81
78
|
}
|
|
82
79
|
// desktop-cli hands the participant a terminal instead of a served page, but it is the
|
|
83
80
|
// same route: same desktop, same actor, same prompt fields. Leaving it out of this predicate told
|
|
@@ -85,13 +82,13 @@ export function isComputerUseComposition(config) {
|
|
|
85
82
|
// person reads a screen.
|
|
86
83
|
if (config.subject.source === "desktop-cli") {
|
|
87
84
|
return ((config.execution?.target === undefined || config.execution.target === "e2b-desktop") &&
|
|
88
|
-
actorResolvesToComputerUse(config
|
|
85
|
+
actorResolvesToComputerUse(actorOf(config)?.type));
|
|
89
86
|
}
|
|
90
87
|
// local-tree packs+uploads the working tree, then serves it exactly like a computer-use clone
|
|
91
88
|
// subject: same e2b-desktop + computer-use-actor gate.
|
|
92
89
|
return ((config.subject.source === "clone" || config.subject.source === "local-tree") &&
|
|
93
90
|
config.execution?.target === "e2b-desktop" &&
|
|
94
|
-
actorResolvesToComputerUse(config
|
|
91
|
+
actorResolvesToComputerUse(actorOf(config)?.type));
|
|
95
92
|
}
|
|
96
93
|
/**
|
|
97
94
|
* True when this config routes to the shared-world route: a clone or local-tree subject
|
|
@@ -108,9 +105,9 @@ export function isSharedWorldComposition(config) {
|
|
|
108
105
|
/** The getHost provisioned-subject shared-world shape (clone/local-tree served + exposed in-sandbox). */
|
|
109
106
|
export function isProvisionedSharedWorldComposition(config) {
|
|
110
107
|
return ((config.subject.source === "clone" || config.subject.source === "local-tree") &&
|
|
111
|
-
config
|
|
108
|
+
declaresSharedWorld(config) &&
|
|
112
109
|
config.execution?.target === "e2b-desktop" &&
|
|
113
|
-
actorResolvesToComputerUse(config
|
|
110
|
+
actorResolvesToComputerUse(actorOf(config)?.type));
|
|
114
111
|
}
|
|
115
112
|
/**
|
|
116
113
|
* The external-public shared-world shape: a real public deployment used directly as
|
|
@@ -123,9 +120,9 @@ export function isProvisionedSharedWorldComposition(config) {
|
|
|
123
120
|
*/
|
|
124
121
|
export function isExternalPublicSharedWorldComposition(config) {
|
|
125
122
|
return (config.subject.source === "app-url" &&
|
|
126
|
-
config
|
|
123
|
+
declaresSharedWorld(config) &&
|
|
127
124
|
config.execution?.target === "e2b-desktop" &&
|
|
128
|
-
actorResolvesToComputerUse(config
|
|
125
|
+
actorResolvesToComputerUse(actorOf(config)?.type) &&
|
|
129
126
|
config.policies?.allowPublicTargets === true);
|
|
130
127
|
}
|
|
131
128
|
/**
|
|
@@ -162,12 +159,12 @@ export function isScriptedBrowserComposition(config) {
|
|
|
162
159
|
return (isLocalScriptedBrowserComposition(config) || isProvisionedScriptedBrowserComposition(config));
|
|
163
160
|
}
|
|
164
161
|
function isLocalScriptedBrowserComposition(config) {
|
|
165
|
-
return (config.subject.source === "app-url" && actorResolvesToScriptedBrowser(config
|
|
162
|
+
return (config.subject.source === "app-url" && actorResolvesToScriptedBrowser(actorOf(config)?.type));
|
|
166
163
|
}
|
|
167
164
|
export function isProvisionedScriptedBrowserComposition(config) {
|
|
168
165
|
return (config.subject.source === "clone" &&
|
|
169
166
|
config.execution?.target === "e2b-desktop" &&
|
|
170
|
-
actorResolvesToScriptedBrowser(config
|
|
167
|
+
actorResolvesToScriptedBrowser(actorOf(config)?.type));
|
|
171
168
|
}
|
|
172
169
|
/**
|
|
173
170
|
* True when this config routes to the terminal-product backend: a terminal-product subject whose
|
|
@@ -176,7 +173,7 @@ export function isProvisionedScriptedBrowserComposition(config) {
|
|
|
176
173
|
* the single source of truth for routeOf and the warning logic.
|
|
177
174
|
*/
|
|
178
175
|
export function isTerminalProductComposition(config) {
|
|
179
|
-
return (config.subject.source === "terminal-product" && actorResolvesToTerminal(config
|
|
176
|
+
return (config.subject.source === "terminal-product" && actorResolvesToTerminal(actorOf(config)?.type));
|
|
180
177
|
}
|
|
181
178
|
/**
|
|
182
179
|
* The route a config takes. It never refuses: a config no route can run still gets the route
|
|
@@ -6,8 +6,8 @@ import path from "node:path";
|
|
|
6
6
|
import { isLocalBrowserStudy } from "../substrates/local/runtime-config.js";
|
|
7
7
|
import { resolveStudyDryRun } from "./plan.js";
|
|
8
8
|
import { knownSecretValues, studyEventEmitter } from "./run-study-events.js";
|
|
9
|
-
import { rosterOf } from "./parse/actors.js";
|
|
10
9
|
import { studyResultIdentity } from "../run/study-result.js";
|
|
10
|
+
import { actorOf, participantList } from "./study-fields.js";
|
|
11
11
|
const unsupported = (option, route, reason) => ({
|
|
12
12
|
ok: false,
|
|
13
13
|
code: "HUMANISH_STUDY_OPTION_UNSUPPORTED",
|
|
@@ -105,7 +105,7 @@ export function normalizeRunStudyOptions(config, route, options) {
|
|
|
105
105
|
export function optionRefusalOutcome(config, route, options, refusal) {
|
|
106
106
|
const cwd = path.resolve(options.cwd);
|
|
107
107
|
const error = { code: refusal.code, message: refusal.message };
|
|
108
|
-
const actor = config
|
|
108
|
+
const actor = actorOf(config)?.type ?? "";
|
|
109
109
|
const dryRun = resolveStudyDryRun(config, options.dryRun, true) ?? true;
|
|
110
110
|
const runId = options.runId ?? "not-created";
|
|
111
111
|
const common = { ok: false, cwd, actor, dryRun, runId, warnings: [], error };
|
|
@@ -154,7 +154,7 @@ export function optionRefusalOutcome(config, route, options, refusal) {
|
|
|
154
154
|
},
|
|
155
155
|
};
|
|
156
156
|
case "shared-world": {
|
|
157
|
-
const participantCount =
|
|
157
|
+
const participantCount = participantList(config)?.length ?? 0;
|
|
158
158
|
return {
|
|
159
159
|
route: "shared-world",
|
|
160
160
|
result: {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { StudyRoute } from "./routing.js";
|
|
2
|
+
import type { StudyActor, StudyConfig, StudyParticipantEntry, StudyScenario, StudyScenarioCaps } from "./types.js";
|
|
3
|
+
/** The actor that runs. A v3 file declares it as `actor`. */
|
|
4
|
+
export declare function actorOf(config: StudyConfig): StudyActor | undefined;
|
|
5
|
+
/**
|
|
6
|
+
* Every declared actor. parseStudy accepts exactly one; a library config may declare more, and the
|
|
7
|
+
* checks that refuse a second actor's settings read them all.
|
|
8
|
+
*/
|
|
9
|
+
export declare function actorsOf(config: StudyConfig): readonly StudyActor[];
|
|
10
|
+
/** The participants listed one by one: a v3 `participants` list. Undefined when none is listed. */
|
|
11
|
+
export declare function participantList(config: StudyConfig): readonly StudyParticipantEntry[] | undefined;
|
|
12
|
+
/** The declared number of identical participants: v3 `participants: <n>` or `participants.count`. */
|
|
13
|
+
export declare function declaredParticipantCount(config: StudyConfig): number | undefined;
|
|
14
|
+
/** The steer each of a counted group of participants gets: v3 `participants.instruction`. */
|
|
15
|
+
export declare function participantInstruction(config: StudyConfig): string | undefined;
|
|
16
|
+
/** How many scripted surfaces run, 1 for desktop and 2 with mobile. A v3 file lists `surfaces`. */
|
|
17
|
+
export declare function surfaceCount(config: StudyConfig): number | undefined;
|
|
18
|
+
/** The declared run mode. A v3 file declares it as `mode`. */
|
|
19
|
+
export declare function modeOf(config: StudyConfig): StudyScenario["mode"];
|
|
20
|
+
/** The scripted route's scenario id or path. A v3 file declares it as `scenario`. */
|
|
21
|
+
export declare function scenarioRefOf(config: StudyConfig): string | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* The spend caps a route enforces. A v3 file declares them as `caps`. The v2 shape keeps them in
|
|
24
|
+
* `execution.caps` on computer use and shared world, and in `scenario.caps` on terminal.
|
|
25
|
+
*/
|
|
26
|
+
export declare function capsOf(config: StudyConfig, route: Extract<StudyRoute, "computer-use" | "shared-world" | "terminal">): StudyScenarioCaps | undefined;
|
|
27
|
+
/** True when the participants share one app. A v3 file declares `route: shared-world`. */
|
|
28
|
+
export declare function declaresSharedWorld(config: StudyConfig): boolean;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// The study's fields, read in one place. StudyConfig still has the humanish.lab.v2 shape
|
|
2
|
+
// (`actors[0]`, `execution.caps`, `scenario.mode`), while a humanish.study.v3 file names the same
|
|
3
|
+
// values `actor`, `participants`, `surfaces`, `caps`, `mode`, `scenario` and `route`. Code outside
|
|
4
|
+
// the parser reads them through these functions, and tests/surface/study-config-reads.test.ts
|
|
5
|
+
// refuses a direct read, so the config can take the v3 shape by changing this file.
|
|
6
|
+
/** The actor that runs. A v3 file declares it as `actor`. */
|
|
7
|
+
export function actorOf(config) {
|
|
8
|
+
return config.actors[0];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Every declared actor. parseStudy accepts exactly one; a library config may declare more, and the
|
|
12
|
+
* checks that refuse a second actor's settings read them all.
|
|
13
|
+
*/
|
|
14
|
+
export function actorsOf(config) {
|
|
15
|
+
return config.actors;
|
|
16
|
+
}
|
|
17
|
+
/** The participants listed one by one: a v3 `participants` list. Undefined when none is listed. */
|
|
18
|
+
export function participantList(config) {
|
|
19
|
+
return config.actors[0]?.lanes;
|
|
20
|
+
}
|
|
21
|
+
/** The declared number of identical participants: v3 `participants: <n>` or `participants.count`. */
|
|
22
|
+
export function declaredParticipantCount(config) {
|
|
23
|
+
return config.actors[0]?.count;
|
|
24
|
+
}
|
|
25
|
+
/** The steer each of a counted group of participants gets: v3 `participants.instruction`. */
|
|
26
|
+
export function participantInstruction(config) {
|
|
27
|
+
return config.actors[0]?.laneFocus?.instruction;
|
|
28
|
+
}
|
|
29
|
+
/** How many scripted surfaces run, 1 for desktop and 2 with mobile. A v3 file lists `surfaces`. */
|
|
30
|
+
export function surfaceCount(config) {
|
|
31
|
+
return config.actors[0]?.count;
|
|
32
|
+
}
|
|
33
|
+
/** The declared run mode. A v3 file declares it as `mode`. */
|
|
34
|
+
export function modeOf(config) {
|
|
35
|
+
return config.scenario?.mode;
|
|
36
|
+
}
|
|
37
|
+
/** The scripted route's scenario id or path. A v3 file declares it as `scenario`. */
|
|
38
|
+
export function scenarioRefOf(config) {
|
|
39
|
+
return config.scenario?.ref;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The spend caps a route enforces. A v3 file declares them as `caps`. The v2 shape keeps them in
|
|
43
|
+
* `execution.caps` on computer use and shared world, and in `scenario.caps` on terminal.
|
|
44
|
+
*/
|
|
45
|
+
export function capsOf(config, route) {
|
|
46
|
+
return route === "terminal" ? config.scenario?.caps : config.execution?.caps;
|
|
47
|
+
}
|
|
48
|
+
/** True when the participants share one app. A v3 file declares `route: shared-world`. */
|
|
49
|
+
export function declaresSharedWorld(config) {
|
|
50
|
+
return config.subject.topology === "shared-world";
|
|
51
|
+
}
|