humanish 0.110.3 → 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/CHANGELOG.md +14 -0
- 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 +4 -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/plan.js +6 -5
- package/dist/routes/computer-use/route.js +2 -1
- package/dist/routes/computer-use/setup.js +2 -1
- package/dist/routes/scripted/plan.js +5 -4
- package/dist/routes/scripted/route.js +5 -1
- package/dist/routes/scripted/surface-sessions.d.ts +2 -0
- package/dist/routes/scripted/surface-sessions.js +2 -1
- 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.js +3 -3
- package/dist/routes/terminal/plan.js +4 -3
- package/dist/routes/terminal/route.js +2 -1
- package/dist/routes/terminal/session.js +1 -0
- 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/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/study/composition-rules.js +32 -32
- 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/{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/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/contracts/schemas.md +13 -4
- package/package.json +1 -1
- /package/dist/study/{owned-files.d.ts → migrate/owned-files.d.ts} +0 -0
package/dist/study/summary.js
CHANGED
|
@@ -18,7 +18,7 @@ import { inspectStudyManifest } from "./discover.js";
|
|
|
18
18
|
import { isComputerUseComposition } from "./routing.js";
|
|
19
19
|
import { probeKeySources } from "../keys/key-resolution.js";
|
|
20
20
|
import { receivingRequiredKey } from "../comms/setup.js";
|
|
21
|
-
import {
|
|
21
|
+
import { actorOf, participantList, declaredParticipantCount, capsOf } from "./study-fields.js";
|
|
22
22
|
export const STUDY_SUMMARY_SCHEMA = "humanish.study-summary.v1";
|
|
23
23
|
/**
|
|
24
24
|
* The effort every participant will run at, or `"per-lane"` when they differ. A participant that declares nothing
|
|
@@ -26,10 +26,8 @@ export const STUDY_SUMMARY_SCHEMA = "humanish.study-summary.v1";
|
|
|
26
26
|
* reported as the resolved value, exactly as `model` reports its default rather than hiding it.
|
|
27
27
|
*/
|
|
28
28
|
function reasoningEffortOf(config) {
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const fallback = actor?.reasoningEffort ?? DEFAULT_OPENAI_CU_REASONING_EFFORT;
|
|
32
|
-
const roster = rosterOf(actor) ?? [];
|
|
29
|
+
const fallback = actorOf(config)?.reasoningEffort ?? DEFAULT_OPENAI_CU_REASONING_EFFORT;
|
|
30
|
+
const roster = participantList(config) ?? [];
|
|
33
31
|
const resolved = new Set(roster.map((entry) => entry.reasoningEffort ?? fallback));
|
|
34
32
|
if (resolved.size > 1)
|
|
35
33
|
return "per-lane";
|
|
@@ -49,15 +47,14 @@ function subjectOf(config) {
|
|
|
49
47
|
}
|
|
50
48
|
/** How many participants, and who; collapsed when they are all the same persona. */
|
|
51
49
|
function participantsOf(config) {
|
|
52
|
-
const
|
|
53
|
-
const actor = actors?.[0];
|
|
50
|
+
const actor = actorOf(config);
|
|
54
51
|
if (actor === undefined)
|
|
55
52
|
return undefined;
|
|
56
|
-
const rosterPersonas = (
|
|
53
|
+
const rosterPersonas = (participantList(config) ?? [])
|
|
57
54
|
.map((entry) => entry.persona)
|
|
58
55
|
.filter((persona) => typeof persona === "string");
|
|
59
56
|
const personas = rosterPersonas.length > 0 ? rosterPersonas : actor.persona === undefined ? [] : [actor.persona];
|
|
60
|
-
const count =
|
|
57
|
+
const count = declaredParticipantCount(config) ?? participantList(config)?.length ?? personas.length ?? 1;
|
|
61
58
|
const unique = [...new Set(personas)];
|
|
62
59
|
if (unique.length === 0)
|
|
63
60
|
return `${count} participant${count === 1 ? "" : "s"}`;
|
|
@@ -66,10 +63,10 @@ function participantsOf(config) {
|
|
|
66
63
|
return unique.length === 1 ? `${count} × ${unique[0]}` : unique.join(" · ");
|
|
67
64
|
}
|
|
68
65
|
/** The computer-use caps; `execution.caps` is inert on other routes, so none is drawn there. */
|
|
69
|
-
function
|
|
66
|
+
function summaryCapsOf(config) {
|
|
70
67
|
if (!isComputerUseComposition(config))
|
|
71
68
|
return {};
|
|
72
|
-
const caps = config
|
|
69
|
+
const caps = capsOf(config, "computer-use");
|
|
73
70
|
return {
|
|
74
71
|
...(typeof caps?.maxUsd === "number" ? { laneUsd: caps.maxUsd } : {}),
|
|
75
72
|
...(typeof caps?.maxTotalUsd === "number" ? { studyUsd: caps.maxTotalUsd } : {}),
|
|
@@ -84,7 +81,6 @@ export async function readStudySummary(cwd, study, options = {}) {
|
|
|
84
81
|
if (inspected === null || !inspected.ok || inspected.config === undefined)
|
|
85
82
|
return null;
|
|
86
83
|
const config = inspected.config;
|
|
87
|
-
const actors = config.actors;
|
|
88
84
|
const route = routeOf(inspected.config);
|
|
89
85
|
let keysReady;
|
|
90
86
|
let missingKeys;
|
|
@@ -128,7 +124,7 @@ export async function readStudySummary(cwd, study, options = {}) {
|
|
|
128
124
|
// Computed once: a test-then-use pair reads as though the two calls could differ.
|
|
129
125
|
const analysis = automaticAnalysisBudget(inspected.config.review?.analysis, route);
|
|
130
126
|
const subject = subjectOf(config);
|
|
131
|
-
const participants = participantsOf(config);
|
|
127
|
+
const participants = participantsOf(inspected.config);
|
|
132
128
|
const runtime = options.checkKeys === true && isLocalBrowserStudy(inspected.config)
|
|
133
129
|
? await localRuntimeStatus({
|
|
134
130
|
...(options.env ? { env: options.env } : {}),
|
|
@@ -138,7 +134,7 @@ export async function readStudySummary(cwd, study, options = {}) {
|
|
|
138
134
|
: undefined;
|
|
139
135
|
const participantReadiness = options.checkKeys === true &&
|
|
140
136
|
isLocalBrowserStudy(inspected.config) &&
|
|
141
|
-
inspected.config
|
|
137
|
+
actorOf(inspected.config)?.type === "local-agent"
|
|
142
138
|
? await localCodexParticipantCheck({ env: options.env ?? process.env })
|
|
143
139
|
: undefined;
|
|
144
140
|
return {
|
|
@@ -165,9 +161,9 @@ export async function readStudySummary(cwd, study, options = {}) {
|
|
|
165
161
|
...(typeof config.description === "string" ? { description: config.description.trim() } : {}),
|
|
166
162
|
...(subject === undefined ? {} : { subject }),
|
|
167
163
|
...(participants === undefined ? {} : { participants }),
|
|
168
|
-
model:
|
|
169
|
-
reasoningEffort: reasoningEffortOf(config),
|
|
170
|
-
caps:
|
|
164
|
+
model: actorOf(inspected.config)?.model ?? DEFAULT_OPENAI_CU_MODEL,
|
|
165
|
+
reasoningEffort: reasoningEffortOf(inspected.config),
|
|
166
|
+
caps: summaryCapsOf(inspected.config),
|
|
171
167
|
...(keysReady === undefined ? {} : { keysReady }),
|
|
172
168
|
...(missingKeys === undefined ? {} : { missingKeys }),
|
|
173
169
|
...(planRefusal === undefined ? {} : { planRefusal }),
|
package/dist/study/types.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type StudyAnalysis } from "../analysis/automatic-config.js";
|
|
|
2
2
|
import type { StudyTask } from "./tasks.js";
|
|
3
3
|
import type { DwellWindow, StopWhen } from "../actors/stop-conditions.js";
|
|
4
4
|
import { type ReasoningEffort } from "../actors/reasoning-effort.js";
|
|
5
|
+
import type { StudyRoute } from "./routing.js";
|
|
5
6
|
export declare const V2_SCHEMA = "humanish.lab.v2";
|
|
6
7
|
/** The study format: it declares its route, one actor, its participants and one caps block. */
|
|
7
8
|
export declare const STUDY_SCHEMA = "humanish.study.v3";
|
|
@@ -805,6 +806,45 @@ export interface StudyConfig {
|
|
|
805
806
|
defaults?: StudyDefaults;
|
|
806
807
|
comms?: StudyComms;
|
|
807
808
|
}
|
|
809
|
+
/**
|
|
810
|
+
* A humanish.study.v3 file as src/study/parse/study.ts reads it: the declared route, one actor, its
|
|
811
|
+
* participants and one caps block. It has the keys the file has. parseStudy still returns the
|
|
812
|
+
* StudyConfig above.
|
|
813
|
+
*/
|
|
814
|
+
export interface StudyV3 {
|
|
815
|
+
schema: typeof STUDY_SCHEMA;
|
|
816
|
+
id: string;
|
|
817
|
+
title?: string;
|
|
818
|
+
description?: string;
|
|
819
|
+
route: StudyRoute;
|
|
820
|
+
/** Absent: a dry run. */
|
|
821
|
+
mode?: StudyScenarioMode;
|
|
822
|
+
subject: Omit<StudySubject, "topology">;
|
|
823
|
+
actor: Omit<StudyActor, "count" | "lanes" | "laneFocus">;
|
|
824
|
+
/** Preview, computer-use and shared-world, in the form the file used. */
|
|
825
|
+
participants?: StudyParticipants;
|
|
826
|
+
/** Scripted only. */
|
|
827
|
+
surfaces?: StudySurfaces;
|
|
828
|
+
/** The caps keys the route reads. */
|
|
829
|
+
caps?: StudyScenarioCaps;
|
|
830
|
+
execution?: Omit<StudyExecution, "caps">;
|
|
831
|
+
/** Scripted: a committed scenario id or path. */
|
|
832
|
+
scenario?: string;
|
|
833
|
+
policies?: StudyPolicies;
|
|
834
|
+
review?: StudyReview;
|
|
835
|
+
defaults?: StudyDefaults;
|
|
836
|
+
comms?: StudyComms;
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* A count, a count with one instruction for every participant, or a list. A list entry with a
|
|
840
|
+
* `count` is expanded into its participants, `<id>-01` to `<id>-NN`.
|
|
841
|
+
*/
|
|
842
|
+
type StudyParticipants = number | StudyParticipantGroup | StudyParticipantEntry[];
|
|
843
|
+
interface StudyParticipantGroup {
|
|
844
|
+
count?: number;
|
|
845
|
+
instruction?: string;
|
|
846
|
+
}
|
|
847
|
+
type StudySurfaces = readonly ["desktop"] | readonly ["desktop", "mobile"];
|
|
808
848
|
interface StudyParseSuccess {
|
|
809
849
|
ok: true;
|
|
810
850
|
config: StudyConfig;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// library caller's config skips the parser that refuses these first.
|
|
3
3
|
import { rosterOf } from "./parse/actors.js";
|
|
4
4
|
import { entryCredentialReason, urlCredentialReason } from "./parse/url-credentials.js";
|
|
5
|
+
import { actorsOf } from "./study-fields.js";
|
|
5
6
|
/**
|
|
6
7
|
* The first URL in `config` that carries a credential, with why: the subject's app, serve and
|
|
7
8
|
* public surface URLs, and each participant's target and entry.
|
|
@@ -16,7 +17,7 @@ export function studyUrlCredentialReason(config) {
|
|
|
16
17
|
? undefined
|
|
17
18
|
: urlCredentialReason("subject.serve.url", subject.serve.url),
|
|
18
19
|
...(subject.product?.publicSurfaces ?? []).map((surface) => () => urlCredentialReason("subject.product.publicSurfaces", surface)),
|
|
19
|
-
...config.
|
|
20
|
+
...actorsOf(config).flatMap((actor) => (rosterOf(actor) ?? []).flatMap((participant, index) => [
|
|
20
21
|
() => participant.target === undefined
|
|
21
22
|
? undefined
|
|
22
23
|
: urlCredentialReason(`participants[${index}].target`, participant.target),
|
|
@@ -13,6 +13,8 @@ export declare function computerUseValidationReason(config: StudyConfig): string
|
|
|
13
13
|
* runStudyWith takes a config that skipped the parser.
|
|
14
14
|
*/
|
|
15
15
|
export declare function sharedWorldValidationReason(config: StudyConfig): string | null;
|
|
16
|
+
/** SMTP capture runs only where each participant has its own world, so a shared world refuses it. */
|
|
17
|
+
export declare function smtpValidationReason(config: StudyConfig): string | undefined;
|
|
16
18
|
/**
|
|
17
19
|
* Declared capture devices must be implemented by the selected execution route.
|
|
18
20
|
* Unsupported backends pass false when called directly, where the declared
|
package/dist/study/validation.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { isMaxOutputTokens } from "../actors/output-token-limit.js";
|
|
2
|
-
import { PARTICIPANT_ID_MAX_CHARS, PARTICIPANT_ID_PATTERN, focusOf
|
|
2
|
+
import { PARTICIPANT_ID_MAX_CHARS, PARTICIPANT_ID_PATTERN, focusOf } from "./parse/actors.js";
|
|
3
3
|
import { isHttpUrl, isLoopbackUrl } from "./parse/subject.js";
|
|
4
4
|
import { declaredTargets } from "./plan-participants.js";
|
|
5
5
|
import { actorResolvesToComputerUse, computerUseParticipantCount, MAX_COMPUTER_USE_PARTICIPANTS, registeredComputerUseActors, resolveEntryUrl, isComputerUseComposition, isScriptedBrowserComposition, isSharedWorldComposition, isTerminalProductComposition, } from "./routing.js";
|
|
6
|
+
import { actorOf, actorsOf, participantList, declaredParticipantCount, declaresSharedWorld, } from "./study-fields.js";
|
|
6
7
|
/**
|
|
7
8
|
* Cross-validate the computer-use fan-out declaration (`per-lane-worlds`). Returns the failure
|
|
8
9
|
* message, or null when valid. The parser enforces it, and runStudyWith checks it again for a config
|
|
@@ -10,8 +11,8 @@ import { actorResolvesToComputerUse, computerUseParticipantCount, MAX_COMPUTER_U
|
|
|
10
11
|
* validity, unique ids) first, then the route-scoped XOR, cap and policy checks.
|
|
11
12
|
*/
|
|
12
13
|
export function computerUseValidationReason(config) {
|
|
13
|
-
const actor = config
|
|
14
|
-
const roster =
|
|
14
|
+
const actor = actorOf(config);
|
|
15
|
+
const roster = participantList(config);
|
|
15
16
|
const structuralReason = rosterStructuralValidationReason(config);
|
|
16
17
|
if (structuralReason) {
|
|
17
18
|
return structuralReason;
|
|
@@ -20,10 +21,10 @@ export function computerUseValidationReason(config) {
|
|
|
20
21
|
// inert-warned). Fan-out is declared via `actors[0].count` or `actors[0].lanes`;
|
|
21
22
|
// subject.clone.fanout never applied here.
|
|
22
23
|
if (config.subject.clone?.fanout !== undefined) {
|
|
23
|
-
return "`subject.clone.fanout` is not used on the computer-use route: declare fan-out with
|
|
24
|
+
return "`subject.clone.fanout` is not used on the computer-use route: declare fan-out with participants (homogeneous) or participants (a roster of participants). (No current route reads clone.fanout.)";
|
|
24
25
|
}
|
|
25
26
|
if (roster !== undefined) {
|
|
26
|
-
if (
|
|
27
|
+
if (declaredParticipantCount(config) !== undefined) {
|
|
27
28
|
return "Set either `actors[0].count` (identical participants) or `actors[0].lanes` (a roster of distinct participants), not both.";
|
|
28
29
|
}
|
|
29
30
|
if (focusOf(actor) !== undefined) {
|
|
@@ -31,18 +32,18 @@ export function computerUseValidationReason(config) {
|
|
|
31
32
|
}
|
|
32
33
|
if (config.execution?.desktop?.resolution !== undefined &&
|
|
33
34
|
roster.some((entry) => entry.device !== undefined)) {
|
|
34
|
-
return "
|
|
35
|
+
return "participants[].device and a raw execution.desktop.resolution are mutually exclusive: a per-participant device preset and a single hand-set resolution cannot both govern participant geometry.";
|
|
35
36
|
}
|
|
36
37
|
const targeted = roster.filter((entry) => entry.target !== undefined);
|
|
37
38
|
if (targeted.length > 0) {
|
|
38
39
|
if (config.subject.source !== "app-url") {
|
|
39
|
-
return "`
|
|
40
|
+
return "`participants[].target` works only on app-url computer-use studies. Clone, shared-world and local-app studies set each participant's entry URL themselves; remove `target`.";
|
|
40
41
|
}
|
|
41
42
|
if (roster.some((entry) => entry.entry !== undefined)) {
|
|
42
|
-
return "
|
|
43
|
+
return "participants[].target and participants[].entry are mutually exclusive: target is an app-url fan-out browser URL; entry is a shared-world same-origin participant path.";
|
|
43
44
|
}
|
|
44
45
|
if (targeted.length !== roster.length) {
|
|
45
|
-
return "When any
|
|
46
|
+
return "When any participants[].target is declared, every participant in the roster must declare target; this keeps the setup-produced target contract explicit and prevents accidental mixed worlds.";
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
}
|
|
@@ -59,8 +60,8 @@ export function computerUseValidationReason(config) {
|
|
|
59
60
|
if (participantCount > 1 &&
|
|
60
61
|
config.policies?.allowPublicTargets === true &&
|
|
61
62
|
declaredTargets(config).length === 0 &&
|
|
62
|
-
config
|
|
63
|
-
return "`policies.allowPublicTargets` with more than one participant sends them all to one public app, which is a shared world. Set `
|
|
63
|
+
!declaresSharedWorld(config)) {
|
|
64
|
+
return "`policies.allowPublicTargets` with more than one participant sends them all to one public app, which is a shared world. Set `route: shared-world` to run them together there, give each `participants[]` entry its own `target`, or run one participant.";
|
|
64
65
|
}
|
|
65
66
|
return null;
|
|
66
67
|
}
|
|
@@ -69,15 +70,15 @@ export function computerUseValidationReason(config) {
|
|
|
69
70
|
* public TypeScript/JavaScript API instead of parseStudy.
|
|
70
71
|
*/
|
|
71
72
|
function rosterStructuralValidationReason(config) {
|
|
72
|
-
const roster =
|
|
73
|
+
const roster = participantList(config);
|
|
73
74
|
const seenIds = new Set();
|
|
74
75
|
if (roster !== undefined) {
|
|
75
76
|
if (!Array.isArray(roster) || roster.length === 0) {
|
|
76
|
-
return "
|
|
77
|
+
return "participants must be a non-empty array when set.";
|
|
77
78
|
}
|
|
78
79
|
for (const [index, entry] of roster.entries()) {
|
|
79
80
|
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
80
|
-
return `
|
|
81
|
+
return `participants[${index}] must be an object.`;
|
|
81
82
|
}
|
|
82
83
|
const id = entry.id;
|
|
83
84
|
if (id === undefined) {
|
|
@@ -86,10 +87,10 @@ function rosterStructuralValidationReason(config) {
|
|
|
86
87
|
if (typeof id !== "string" ||
|
|
87
88
|
!PARTICIPANT_ID_PATTERN.test(id) ||
|
|
88
89
|
id.length > PARTICIPANT_ID_MAX_CHARS) {
|
|
89
|
-
return `
|
|
90
|
+
return `participants[${index}].id must be a public-safe path token matching ${PARTICIPANT_ID_PATTERN} and at most ${PARTICIPANT_ID_MAX_CHARS} chars.`;
|
|
90
91
|
}
|
|
91
92
|
if (seenIds.has(id)) {
|
|
92
|
-
return `
|
|
93
|
+
return `participants ids must be unique (duplicate "${id}").`;
|
|
93
94
|
}
|
|
94
95
|
seenIds.add(id);
|
|
95
96
|
}
|
|
@@ -120,32 +121,38 @@ function provisionedSharedWorldStructureReason(config) {
|
|
|
120
121
|
return structuralReason;
|
|
121
122
|
}
|
|
122
123
|
if (config.subject.source !== "clone" && config.subject.source !== "local-tree") {
|
|
123
|
-
return "`
|
|
124
|
+
return "`route: shared-world` needs `subject.source: clone` or `local-tree`, which humanish serves as one seeded app every participant uses, or `app-url` for a public deployment you own.";
|
|
124
125
|
}
|
|
125
126
|
if (config.execution?.target !== "e2b-desktop") {
|
|
126
|
-
return "`
|
|
127
|
+
return "`route: shared-world` requires `execution.target: e2b-desktop`: the role participants drive hosted desktop browsers against one in-sandbox app.";
|
|
127
128
|
}
|
|
128
|
-
if (!actorResolvesToComputerUse(config
|
|
129
|
-
return `\`
|
|
129
|
+
if (!actorResolvesToComputerUse(actorOf(config)?.type)) {
|
|
130
|
+
return `\`route: shared-world\` requires a registered computer-use actor (one of: ${registeredComputerUseActors().join(", ")}); each role participant runs a computer-use session.`;
|
|
130
131
|
}
|
|
131
132
|
const serve = config.subject.serve;
|
|
132
133
|
if (!serve) {
|
|
133
|
-
return "`
|
|
134
|
+
return "`route: shared-world` needs `subject.serve` (start and url): humanish starts one copy of the app in the sandbox, and every participant uses it.";
|
|
134
135
|
}
|
|
135
|
-
const roster =
|
|
136
|
+
const roster = participantList(config);
|
|
136
137
|
if (!roster || roster.length < 2) {
|
|
137
|
-
return "`
|
|
138
|
+
return "`route: shared-world` needs a `participants` list of at least 2 participants: with one participant there is nobody to interact with.";
|
|
138
139
|
}
|
|
139
140
|
if (!config.subject.state?.checkpoint || config.subject.state.checkpoint.length === 0) {
|
|
140
|
-
return "`
|
|
141
|
+
return "`route: shared-world` needs at least one read-only `subject.state.checkpoint` probe: the checkpoints are how the run shows that participants changed the shared app.";
|
|
141
142
|
}
|
|
142
143
|
for (const entry of roster) {
|
|
143
144
|
if (entry.entry !== undefined && resolveEntryUrl(serve.url, entry.entry) === null) {
|
|
144
|
-
return `
|
|
145
|
+
return `participants role "${entry.id ?? "(unnamed)"}".entry must resolve same-origin (loopback) with subject.serve.url (${serve.url}); got "${entry.entry}".`;
|
|
145
146
|
}
|
|
146
147
|
}
|
|
147
148
|
return null;
|
|
148
149
|
}
|
|
150
|
+
/** SMTP capture runs only where each participant has its own world, so a shared world refuses it. */
|
|
151
|
+
export function smtpValidationReason(config) {
|
|
152
|
+
return config.comms?.email?.smtp && declaresSharedWorld(config)
|
|
153
|
+
? "SMTP capture is not supported yet for shared-world studies. Use the default per-lane-worlds topology for SMTP, or configure supported HTTP email capture for concurrent shared-world studies."
|
|
154
|
+
: undefined;
|
|
155
|
+
}
|
|
149
156
|
/**
|
|
150
157
|
* Declared capture devices must be implemented by the selected execution route.
|
|
151
158
|
* Unsupported backends pass false when called directly, where the declared
|
|
@@ -153,9 +160,7 @@ function provisionedSharedWorldStructureReason(config) {
|
|
|
153
160
|
*/
|
|
154
161
|
export function desktopMediaValidationReason(config, supportsMedia = isComputerUseComposition(config)) {
|
|
155
162
|
if (config.execution?.desktop?.recording !== undefined &&
|
|
156
|
-
(!supportsMedia ||
|
|
157
|
-
config.subject.topology === "shared-world" ||
|
|
158
|
-
config.subject.source === "local-app")) {
|
|
163
|
+
(!supportsMedia || declaresSharedWorld(config) || config.subject.source === "local-app")) {
|
|
159
164
|
return "execution.desktop.recording is supported only for computer-use participants on independent desktops. Remove the declaration or select a supported route.";
|
|
160
165
|
}
|
|
161
166
|
const media = config.execution?.desktop?.media;
|
|
@@ -164,7 +169,7 @@ export function desktopMediaValidationReason(config, supportsMedia = isComputerU
|
|
|
164
169
|
if (media.microphone !== undefined && media.microphone.source !== "speech") {
|
|
165
170
|
return "execution.desktop.media.microphone.source must be speech. Microphone source-file injection is unsupported.";
|
|
166
171
|
}
|
|
167
|
-
if (config
|
|
172
|
+
if (declaresSharedWorld(config)) {
|
|
168
173
|
return "execution.desktop.media is unsupported on shared-world routes; declared capture devices would not be provisioned. Use independent computer-use browser participants or remove the declaration.";
|
|
169
174
|
}
|
|
170
175
|
if (!supportsMedia ||
|
|
@@ -176,7 +181,7 @@ export function desktopMediaValidationReason(config, supportsMedia = isComputerU
|
|
|
176
181
|
return "execution.desktop.media requires Chrome or Chromium; Firefox cannot receive the declared synthetic capture device. Set execution.desktop.browser: chrome or chromium.";
|
|
177
182
|
}
|
|
178
183
|
if (media.microphone !== undefined) {
|
|
179
|
-
if (config.
|
|
184
|
+
if (actorsOf(config).some((actor) => actor.type !== "local-agent" ||
|
|
180
185
|
(actor.localAgent !== undefined && actor.localAgent !== "codex"))) {
|
|
181
186
|
return "Participant speech currently requires local-agent with Codex, on a local or hosted desktop.";
|
|
182
187
|
}
|
|
@@ -194,7 +199,7 @@ export function receivingEmailValidationReason(config) {
|
|
|
194
199
|
!["app-url", "clone", "local-tree"].includes(config.subject.source)) {
|
|
195
200
|
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.";
|
|
196
201
|
}
|
|
197
|
-
if (config.
|
|
202
|
+
if (actorsOf(config).some((actor) => actor.type === "local-agent")) {
|
|
198
203
|
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.";
|
|
199
204
|
}
|
|
200
205
|
return undefined;
|
|
@@ -202,14 +207,14 @@ export function receivingEmailValidationReason(config) {
|
|
|
202
207
|
/** Refuse task declarations that the selected execution path would discard.
|
|
203
208
|
* Direct runners pass their actual support rather than trusting the config's dispatch shape. */
|
|
204
209
|
export function taskProtocolValidationReason(config, supportsTasks = isComputerUseComposition(config) && !isSharedWorldComposition(config)) {
|
|
205
|
-
for (const [index, actor] of config.
|
|
210
|
+
for (const [index, actor] of actorsOf(config).entries()) {
|
|
206
211
|
if (actor.tasks === undefined)
|
|
207
212
|
continue;
|
|
208
213
|
if (index > 0) {
|
|
209
214
|
return `actors[${index}].tasks is unsupported: current runners consume only actors[0]. Use the first actor's computer-use participants for a task protocol.`;
|
|
210
215
|
}
|
|
211
216
|
if (!supportsTasks) {
|
|
212
|
-
return "
|
|
217
|
+
return "actor.tasks is unsupported on this execution path. Task protocols require the computer-use route, with one world per participant; shared-world, terminal-product, scripted-browser and synthetic routes do not consume them. Remove tasks only if a mission-only study is intended.";
|
|
213
218
|
}
|
|
214
219
|
}
|
|
215
220
|
return null;
|
|
@@ -248,15 +253,15 @@ export function scenarioCapsValidationReason(config) {
|
|
|
248
253
|
}
|
|
249
254
|
/** Refuse a claimed output bound when the route cannot pass it to the first-party provider. */
|
|
250
255
|
export function outputTokenLimitValidationReason(config) {
|
|
251
|
-
const actor = config
|
|
256
|
+
const actor = actorOf(config);
|
|
252
257
|
if (actor?.maxOutputTokens === undefined)
|
|
253
258
|
return null;
|
|
254
259
|
if (!isMaxOutputTokens(actor.maxOutputTokens))
|
|
255
|
-
return "
|
|
260
|
+
return "actor.maxOutputTokens must be a positive safe integer.";
|
|
256
261
|
if (actor.type !== "openai-computer-use" ||
|
|
257
262
|
!isComputerUseComposition(config) ||
|
|
258
263
|
config.subject.source === "local-app") {
|
|
259
|
-
return "
|
|
264
|
+
return "actor.maxOutputTokens is supported only by first-party OpenAI computer-use routes; terminal, local-agent, scripted and custom in-process routes cannot enforce it.";
|
|
260
265
|
}
|
|
261
266
|
return null;
|
|
262
267
|
}
|
|
@@ -270,7 +275,7 @@ export function outputTokenLimitValidationReason(config) {
|
|
|
270
275
|
function sharedWorldConcurrencyReason(config) {
|
|
271
276
|
// Direct library callers skip the parser, so an omitted value defaults here exactly as the
|
|
272
277
|
// route does: to the participant count. A missing roster reads as 0 and is refused.
|
|
273
|
-
const participants =
|
|
278
|
+
const participants = participantList(config)?.length ?? 0;
|
|
274
279
|
const concurrency = config.execution?.concurrency ?? participants;
|
|
275
280
|
if (concurrency >= 2)
|
|
276
281
|
return null;
|
|
@@ -327,12 +332,12 @@ export function externalPublicSharedWorldValidationReason(config) {
|
|
|
327
332
|
if (config.execution?.target !== "e2b-desktop") {
|
|
328
333
|
return "the external-public shared-world route requires `execution.target: e2b-desktop`: the role participants drive hosted desktop browsers against the one public deployment.";
|
|
329
334
|
}
|
|
330
|
-
if (!actorResolvesToComputerUse(config
|
|
335
|
+
if (!actorResolvesToComputerUse(actorOf(config)?.type)) {
|
|
331
336
|
return `the external-public shared-world route requires a registered computer-use actor (one of: ${registeredComputerUseActors().join(", ")}); each role participant runs a computer-use session.`;
|
|
332
337
|
}
|
|
333
|
-
const roster =
|
|
338
|
+
const roster = participantList(config);
|
|
334
339
|
if (!roster || roster.length < 2) {
|
|
335
|
-
return "the external-public shared-world route requires
|
|
340
|
+
return "the external-public shared-world route requires a `participants` list of at least 2 roles (a single-participant shared world proves no shared session).";
|
|
336
341
|
}
|
|
337
342
|
const sharedWorldConcurrency = sharedWorldConcurrencyReason(config);
|
|
338
343
|
if (sharedWorldConcurrency)
|
|
@@ -367,7 +372,7 @@ export function externalPublicSharedWorldValidationReason(config) {
|
|
|
367
372
|
return "`subject.clone` and `subject.repos` do not apply to an external-public shared-world study: the participants use the public deployment, so nothing is cloned. Remove them.";
|
|
368
373
|
}
|
|
369
374
|
if (roster.some((entry) => entry.entry !== undefined)) {
|
|
370
|
-
return "`
|
|
375
|
+
return "`participants[].entry` (the loopback same-origin participant path) is forbidden on the external-public shared-world route: there is no harness-served serve.url to resolve it against; participants open the public appUrl and reach the shared session through the real UI.";
|
|
371
376
|
}
|
|
372
377
|
const hostEntries = roster.filter((entry) => entry.host === true);
|
|
373
378
|
if (hostEntries.length !== 1) {
|
package/dist/study/warnings.js
CHANGED
|
@@ -2,6 +2,7 @@ import { isComputerUseComposition, isScriptedBrowserComposition, isSharedWorldCo
|
|
|
2
2
|
import { declaredParticipantIds } from "./plan-participants.js";
|
|
3
3
|
import { addressedRecipients } from "./parse/comms.js";
|
|
4
4
|
import { focusOf, rosterOf } from "./parse/actors.js";
|
|
5
|
+
import { actorsOf, participantList, declaredParticipantCount, scenarioRefOf, } from "./study-fields.js";
|
|
5
6
|
// The computer-use and terminal routes consume mission/persona/model and laneFocus.instruction (they
|
|
6
7
|
// compose the agent prompt and bundle provenance); laneFocus.id/label stay inert. On the computer-use
|
|
7
8
|
// E2B route actors[0].lanes is consumed (the fan-out roster).
|
|
@@ -250,7 +251,7 @@ const CONFIG_ROWS = [
|
|
|
250
251
|
// everywhere else.
|
|
251
252
|
{
|
|
252
253
|
field: "scenario.ref",
|
|
253
|
-
applies: (config, routes) => Boolean(config
|
|
254
|
+
applies: (config, routes) => Boolean(scenarioRefOf(config)) && !routes.scripted,
|
|
254
255
|
},
|
|
255
256
|
{ field: "scenario.inline", applies: (config) => Boolean(config.scenario?.inline) },
|
|
256
257
|
// review.{scoring,milestones,vocabulary} stay forward-declared on every route.
|
|
@@ -306,7 +307,7 @@ function rowLabel(row, prefix = "") {
|
|
|
306
307
|
export function inertFieldLabels(config) {
|
|
307
308
|
const routes = routesOf(config);
|
|
308
309
|
const inert = [];
|
|
309
|
-
for (const [index, actor] of config.
|
|
310
|
+
for (const [index, actor] of actorsOf(config).entries()) {
|
|
310
311
|
for (const row of ACTOR_ROWS)
|
|
311
312
|
if (row.applies(actor, routes))
|
|
312
313
|
inert.push(rowLabel(row, `actors[${index}].`));
|
|
@@ -323,7 +324,7 @@ export function inertFieldLabels(config) {
|
|
|
323
324
|
export function inertFieldPaths(config) {
|
|
324
325
|
const routes = routesOf(config);
|
|
325
326
|
const paths = [];
|
|
326
|
-
for (const [index, actor] of config.
|
|
327
|
+
for (const [index, actor] of actorsOf(config).entries()) {
|
|
327
328
|
for (const row of ACTOR_ROWS)
|
|
328
329
|
if (row.applies(actor, routes))
|
|
329
330
|
paths.push(`actors[${index}].${row.field}`);
|
|
@@ -347,7 +348,7 @@ export function forwardDeclaredWarnings(config) {
|
|
|
347
348
|
// and the cap delivers waves of M. Say so up front (inspect + dry-run + run): a green run in
|
|
348
349
|
// waves is otherwise indistinguishable from the all-live run the author meant.
|
|
349
350
|
{
|
|
350
|
-
const participantCount =
|
|
351
|
+
const participantCount = participantList(config)?.length ?? declaredParticipantCount(config) ?? 1;
|
|
351
352
|
const cap = config.execution?.concurrency;
|
|
352
353
|
if (routes.cua && cap !== undefined && participantCount > 1 && cap < participantCount) {
|
|
353
354
|
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.`);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { actorOf, actorsOf, participantList } from "../../study/study-fields.js";
|
|
2
2
|
// The runtime image enforces a 30-minute lifetime; reserve setup/teardown room.
|
|
3
3
|
export const LOCAL_BROWSER_LIFETIME_MS = 30 * 60_000;
|
|
4
4
|
const MAX_SESSION_MS = 20 * 60_000;
|
|
5
5
|
export function isLocalBrowserStudy(config) {
|
|
6
6
|
return (config.subject.source === "app-url" &&
|
|
7
7
|
config.execution?.target === "local" &&
|
|
8
|
-
["local-agent", "openai-computer-use"].includes(config
|
|
8
|
+
["local-agent", "openai-computer-use"].includes(actorOf(config)?.type ?? ""));
|
|
9
9
|
}
|
|
10
10
|
/** Defaults for the explicitly selected local substrate; hosted configurations are untouched. */
|
|
11
11
|
export function localBrowserDefaults(config) {
|
|
@@ -13,7 +13,7 @@ export function localBrowserDefaults(config) {
|
|
|
13
13
|
return config;
|
|
14
14
|
return {
|
|
15
15
|
...config,
|
|
16
|
-
actors: config.
|
|
16
|
+
actors: actorsOf(config).map((actor) => ({
|
|
17
17
|
...actor,
|
|
18
18
|
model: actor.model ?? "gpt-6-astra",
|
|
19
19
|
reasoningEffort: actor.reasoningEffort ?? "low",
|
|
@@ -26,18 +26,18 @@ export function localBrowserDefaults(config) {
|
|
|
26
26
|
resolution: config.execution?.desktop?.resolution ?? [960, 720],
|
|
27
27
|
},
|
|
28
28
|
},
|
|
29
|
-
...(config.review?.analysis === undefined && config
|
|
29
|
+
...(config.review?.analysis === undefined && actorOf(config)?.type === "local-agent"
|
|
30
30
|
? { review: { ...config.review, analysis: { provider: "codex" } } }
|
|
31
31
|
: {}),
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
34
|
export function localBrowserUnsupportedReason(config) {
|
|
35
|
-
const actor = config
|
|
35
|
+
const actor = actorOf(config);
|
|
36
36
|
const desktop = config.execution?.desktop;
|
|
37
37
|
if ((config.execution?.timeoutMs ?? MAX_SESSION_MS) > MAX_SESSION_MS) {
|
|
38
38
|
return "Local browser sessions currently support at most 20 minutes, within the runtime's 30-minute lifetime.";
|
|
39
39
|
}
|
|
40
|
-
if (config.
|
|
40
|
+
if (actorsOf(config).length !== 1 ||
|
|
41
41
|
!actor ||
|
|
42
42
|
(actor.type === "local-agent" && actor.localAgent !== undefined && actor.localAgent !== "codex")) {
|
|
43
43
|
return "Local browser studies support only Codex local-agent or openai-computer-use participants.";
|
|
@@ -46,7 +46,7 @@ export function localBrowserUnsupportedReason(config) {
|
|
|
46
46
|
desktop.resolution[1] !== 720 ||
|
|
47
47
|
desktop.device !== undefined ||
|
|
48
48
|
(desktop.browser !== undefined && !["default", "chromium"].includes(desktop.browser)) ||
|
|
49
|
-
|
|
49
|
+
participantList(config)?.some((entry) => entry.device !== undefined) ||
|
|
50
50
|
desktop.template !== undefined ||
|
|
51
51
|
desktop.sandboxTimeoutMs !== undefined) {
|
|
52
52
|
return "The local browser runtime currently uses a 960×720 Chromium desktop. Omit hosted templates, device presets and sandboxTimeoutMs.";
|
|
@@ -61,7 +61,7 @@ export function localBrowserUnsupportedReason(config) {
|
|
|
61
61
|
if (actor.type === "local-agent" &&
|
|
62
62
|
(actor.model !== "gpt-6-astra" ||
|
|
63
63
|
actor.reasoningEffort !== "low" ||
|
|
64
|
-
|
|
64
|
+
participantList(config)?.some((entry) => entry.reasoningEffort !== undefined && entry.reasoningEffort !== "low") ||
|
|
65
65
|
actor.maxOutputTokens !== undefined ||
|
|
66
66
|
config.execution?.caps?.maxUsd !== undefined ||
|
|
67
67
|
config.execution?.caps?.maxTotalUsd !== undefined ||
|
|
@@ -71,7 +71,7 @@ export function localBrowserUnsupportedReason(config) {
|
|
|
71
71
|
}
|
|
72
72
|
for (const target of [
|
|
73
73
|
config.subject.appUrl,
|
|
74
|
-
...(
|
|
74
|
+
...(participantList(config) ?? []).map((entry) => entry.target),
|
|
75
75
|
].filter(Boolean)) {
|
|
76
76
|
let url;
|
|
77
77
|
try {
|