humanish 0.116.0 → 0.118.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 +40 -0
- package/dist/actors/codex/restricted-participant-policy.d.ts +0 -2
- package/dist/actors/codex/restricted-participant-policy.js +4 -32
- package/dist/actors/codex/restricted-participant.d.ts +5 -0
- package/dist/actors/codex/restricted-participant.js +3 -2
- package/dist/actors/computer-use/actor.d.ts +4 -0
- package/dist/actors/computer-use/actor.js +2 -0
- package/dist/actors/computer-use/loop/actions.d.ts +4 -4
- package/dist/actors/computer-use/loop/actions.js +25 -18
- package/dist/actors/computer-use/loop/session.d.ts +3 -0
- package/dist/actors/computer-use/loop/session.js +9 -1
- package/dist/actors/computer-use/loop/trace.js +1 -0
- package/dist/actors/computer-use/loop/types.d.ts +16 -14
- package/dist/actors/computer-use/loop.d.ts +1 -1
- package/dist/actors/computer-use/loop.js +1 -1
- package/dist/actors/computer-use/wait.d.ts +74 -0
- package/dist/actors/computer-use/wait.js +100 -0
- package/dist/actors/contract.d.ts +14 -0
- package/dist/analysis/admission.d.ts +7 -2
- package/dist/analysis/admission.js +12 -3
- package/dist/analysis/automatic-config.d.ts +6 -1
- package/dist/analysis/automatic-config.js +12 -11
- package/dist/analysis/execute.d.ts +19 -7
- package/dist/analysis/execute.js +55 -17
- package/dist/browser-control/protocol.d.ts +86 -6
- package/dist/browser-control/protocol.js +13 -6
- package/dist/cli/commands/analyze.js +2 -1
- package/dist/cli/commands/study-format.js +8 -11
- package/dist/cli/commands/study-route-computer-use.js +2 -2
- package/dist/cli/commands/study-route-scripted.js +2 -2
- package/dist/cli/commands/study-route-shared-world.js +5 -3
- package/dist/cli/commands/study-route-terminal.js +2 -2
- package/dist/cli/commands/study-run.js +3 -3
- package/dist/cli/commands/study.js +2 -2
- package/dist/cli/doctor.js +11 -1
- package/dist/cli/io.js +3 -2
- package/dist/comms/lease-store.d.ts +1 -1
- package/dist/comms/lease-store.js +3 -2
- package/dist/comms/receiving-types.d.ts +2 -0
- package/dist/comms/receiving-types.js +2 -0
- package/dist/guest/desktop-executor.js +3 -1
- package/dist/observer-app.html +7 -7
- package/dist/routes/computer-use/e2b-desktop/prepare.js +40 -31
- package/dist/routes/computer-use/fanout-bundle.js +2 -1
- package/dist/routes/computer-use/fanout-records.js +2 -1
- package/dist/routes/computer-use/local-vm.js +5 -2
- package/dist/routes/computer-use/participant-model.js +9 -0
- package/dist/routes/computer-use/participant-runs.js +3 -9
- package/dist/routes/computer-use/plan.d.ts +3 -1
- package/dist/routes/computer-use/plan.js +29 -9
- package/dist/routes/computer-use/single-bundle.js +7 -4
- package/dist/routes/computer-use/subject-projection.js +2 -0
- package/dist/routes/computer-use/types.d.ts +1 -1
- package/dist/routes/scripted/plan.d.ts +4 -1
- package/dist/routes/scripted/plan.js +25 -2
- package/dist/routes/scripted/setup.js +1 -4
- package/dist/routes/scripted/types.d.ts +1 -1
- package/dist/routes/shared-world/participant-specs.js +2 -2
- package/dist/routes/shared-world/plan.d.ts +3 -1
- package/dist/routes/shared-world/plan.js +39 -7
- package/dist/routes/shared-world/route.js +2 -2
- package/dist/routes/terminal/live-sandbox.d.ts +1 -1
- package/dist/routes/terminal/live-sandbox.js +14 -11
- package/dist/routes/terminal/sandbox.d.ts +4 -3
- package/dist/routes/terminal/sandbox.js +5 -5
- package/dist/run/bundle-shape.js +2 -1
- package/dist/run/bundle.d.ts +5 -3
- package/dist/run/projection.js +7 -2
- package/dist/run/route-shell.js +10 -5
- package/dist/run/subject-name.d.ts +11 -0
- package/dist/run/subject-name.js +11 -0
- package/dist/study/automatic-analysis-plan.d.ts +28 -0
- package/dist/study/automatic-analysis-plan.js +57 -0
- package/dist/study/composition-rules.js +1 -1
- package/dist/study/config.js +10 -21
- package/dist/study/doctor.js +17 -3
- package/dist/study/keys.d.ts +2 -0
- package/dist/study/keys.js +2 -0
- package/dist/study/migrate/v2.js +2 -1
- package/dist/study/parse/actors.d.ts +1 -1
- package/dist/study/parse/actors.js +9 -1
- package/dist/study/parse/front.d.ts +1 -1
- package/dist/study/parse/front.js +7 -1
- package/dist/study/plan-base.d.ts +2 -0
- package/dist/study/plan-base.js +3 -0
- package/dist/study/plan-participants.d.ts +4 -0
- package/dist/study/plan-participants.js +6 -5
- package/dist/study/plan-types.d.ts +8 -3
- package/dist/study/plan.d.ts +4 -4
- package/dist/study/plan.js +21 -12
- package/dist/study/preflight-result.js +2 -2
- package/dist/study/preflight.d.ts +2 -1
- package/dist/study/preflight.js +12 -3
- package/dist/study/routing.d.ts +5 -4
- package/dist/study/routing.js +16 -7
- package/dist/study/summary.d.ts +3 -3
- package/dist/study/summary.js +3 -5
- package/dist/study/types.d.ts +14 -0
- package/dist/study/validation.d.ts +7 -2
- package/dist/study/validation.js +30 -14
- package/dist/study/warnings.js +12 -0
- package/dist/subject/serve.d.ts +3 -0
- package/dist/subject/serve.js +4 -1
- package/dist/substrates/detached.js +14 -7
- package/dist/substrates/e2b/desktop-executor.d.ts +0 -2
- package/dist/substrates/e2b/desktop-executor.js +3 -3
- package/dist/substrates/e2b/lifetime.d.ts +75 -4
- package/dist/substrates/e2b/lifetime.js +134 -10
- package/dist/substrates/e2b/subject-sandbox.js +2 -6
- package/dist/tui-app.js +119 -118
- package/dist/verify/artifacts.d.ts +19 -4
- package/dist/verify/artifacts.js +39 -8
- package/dist/verify/verify.js +6 -15
- package/docs/architecture/browser-control.md +10 -4
- package/docs/contracts/run-bundle.md +7 -6
- package/docs/contracts/schemas.md +33 -19
- package/docs/contracts/study-analysis.md +2 -2
- package/docs/principles/invariants-and-defaults.md +1 -1
- package/docs/product/automatic-analysis.md +4 -3
- package/package.json +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Who takes part in a study: one record per participant, built from the declared roster or a count.
|
|
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
|
-
import { isSharedWorldComposition, participantIdAt } from "./routing.js";
|
|
4
|
+
import { computerUseParticipantCount, isSharedWorldComposition, participantIdAt, } from "./routing.js";
|
|
5
5
|
import { participantList, declaredParticipantCount, participantInstruction, } from "./study-fields.js";
|
|
6
6
|
function desktopParticipant(config, entry, index, kind, focus) {
|
|
7
7
|
const actor = config.actor;
|
|
@@ -12,6 +12,8 @@ function desktopParticipant(config, entry, index, kind, focus) {
|
|
|
12
12
|
const dwell = entry?.dwell ?? actor?.dwell;
|
|
13
13
|
const reasoningEffort = entry?.reasoningEffort ?? actor?.reasoningEffort;
|
|
14
14
|
const maxOutputTokens = actor?.maxOutputTokens;
|
|
15
|
+
const maxWaitMs = actor?.maxWaitMs;
|
|
16
|
+
const idleWaitMs = actor?.idleWaitMs;
|
|
15
17
|
return {
|
|
16
18
|
id: participantIdAt(index, entry?.id, kind),
|
|
17
19
|
index,
|
|
@@ -31,6 +33,8 @@ function desktopParticipant(config, entry, index, kind, focus) {
|
|
|
31
33
|
...(dwell === undefined ? {} : { dwell }),
|
|
32
34
|
...(reasoningEffort === undefined ? {} : { reasoningEffort }),
|
|
33
35
|
...(maxOutputTokens === undefined ? {} : { maxOutputTokens }),
|
|
36
|
+
...(maxWaitMs === undefined ? {} : { maxWaitMs }),
|
|
37
|
+
...(idleWaitMs === undefined ? {} : { idleWaitMs }),
|
|
34
38
|
},
|
|
35
39
|
};
|
|
36
40
|
}
|
|
@@ -41,10 +45,7 @@ function desktopParticipant(config, entry, index, kind, focus) {
|
|
|
41
45
|
export function computerUseParticipants(config, countOverride) {
|
|
42
46
|
const actor = config.actor;
|
|
43
47
|
const roster = participantList(config);
|
|
44
|
-
|
|
45
|
-
? roster.length
|
|
46
|
-
: Math.max(1, countOverride ?? declaredParticipantCount(config) ?? 1);
|
|
47
|
-
return Array.from({ length: count }, (_, index) => {
|
|
48
|
+
return Array.from({ length: computerUseParticipantCount(config, countOverride) }, (_, index) => {
|
|
48
49
|
const entry = roster?.[index];
|
|
49
50
|
const focus = roster ? entry?.instruction : participantInstruction(config);
|
|
50
51
|
return {
|
|
@@ -57,13 +57,17 @@ export interface PlannedAnalysis {
|
|
|
57
57
|
/** "default" when the study declared no review.analysis. */
|
|
58
58
|
readonly trigger: "default" | "explicit";
|
|
59
59
|
readonly preferLargerOutput: boolean;
|
|
60
|
+
/** Set when the study has more participants than one analysis reads: a live run records the
|
|
61
|
+
* analysis as skipped for this reason and never starts it. */
|
|
62
|
+
readonly skip?: "AUTOMATIC_ANALYSIS_PARTICIPANT_LIMIT";
|
|
60
63
|
}
|
|
61
64
|
interface PlanBase {
|
|
62
65
|
readonly studyId: string;
|
|
63
66
|
/** The study's declared title, which bundles record. */
|
|
64
67
|
readonly title?: string;
|
|
65
68
|
readonly study?: RunStudyProvenance;
|
|
66
|
-
/** Warnings about the study's own fields
|
|
69
|
+
/** Warnings about the study's own fields and the plan made from them, which the run records in
|
|
70
|
+
* its bundle as warn events. */
|
|
67
71
|
readonly warnings?: readonly string[];
|
|
68
72
|
/** A frozen copy owned by the plan. */
|
|
69
73
|
readonly residual: Readonly<ResidualConfig>;
|
|
@@ -258,9 +262,10 @@ export interface ScriptedPlan extends PlanBase {
|
|
|
258
262
|
readonly scenarioRef: string;
|
|
259
263
|
/** Empty only for a library caller's `count: 0`, which the parser refuses; that run has no sessions. */
|
|
260
264
|
readonly surfaces: readonly BrowserSurface[];
|
|
261
|
-
/** The declared persona
|
|
265
|
+
/** The declared persona; the route supplies its default. */
|
|
262
266
|
readonly personaId?: string;
|
|
263
|
-
|
|
267
|
+
/** Each surface's journey budget: execution.timeoutMs, else 5 minutes. */
|
|
268
|
+
readonly sessionTimeoutMs: number;
|
|
264
269
|
}
|
|
265
270
|
export type StudyPlan = PreviewPlan | ComputerUsePlan | SharedWorldPlan | TerminalPlan | ScriptedPlan;
|
|
266
271
|
interface PlannedStudy {
|
package/dist/study/plan.d.ts
CHANGED
|
@@ -7,9 +7,9 @@ export { routeOf, type StudyRoute } from "./routing.js";
|
|
|
7
7
|
export declare function resolveStudyDryRun(config: StudyConfig, override: boolean | undefined, fallback: boolean | undefined): boolean | undefined;
|
|
8
8
|
/**
|
|
9
9
|
* The plan a study runs under, built without reading files or the network. From the environment
|
|
10
|
-
* (options.env, else process.env, as the routes read it) it reads
|
|
11
|
-
* ceiling, which no study file can know. Each route's
|
|
12
|
-
* makes, in the route's order and with its codes and
|
|
13
|
-
* the same planner.
|
|
10
|
+
* (options.env, else process.env, as the routes read it) it reads the operator's E2B plan limits,
|
|
11
|
+
* the sandbox ceiling and the concurrent sandboxes, which no study file can know. Each route's
|
|
12
|
+
* planner makes every refusal that route makes, in the route's order and with its codes and
|
|
13
|
+
* messages; the route's exported runner calls the same planner.
|
|
14
14
|
*/
|
|
15
15
|
export declare function planStudy(config: StudyConfig, options: InternalRunStudyOptions, deps?: StudyDeps): PlanResult;
|
package/dist/study/plan.js
CHANGED
|
@@ -6,7 +6,7 @@ import { callerDrivingOf, planComputerUseStudy } from "../routes/computer-use/pl
|
|
|
6
6
|
import { injectedBrowser, planScriptedStudy } from "../routes/scripted/plan.js";
|
|
7
7
|
import { planSharedWorldStudy } from "../routes/shared-world/plan.js";
|
|
8
8
|
import { planTerminalStudy } from "../routes/terminal/plan.js";
|
|
9
|
-
import { sandboxCeiling } from "../substrates/e2b/lifetime.js";
|
|
9
|
+
import { concurrentSandboxes, sandboxCeiling, } from "../substrates/e2b/lifetime.js";
|
|
10
10
|
import { localBrowserDefaults } from "../substrates/local/runtime-config.js";
|
|
11
11
|
import { THIS_REPO_DRY_RUN_ONLY } from "./composition-rules.js";
|
|
12
12
|
import { planBase } from "./plan-base.js";
|
|
@@ -65,21 +65,25 @@ function planPreview(config, options, input) {
|
|
|
65
65
|
}
|
|
66
66
|
/**
|
|
67
67
|
* The plan a study runs under, built without reading files or the network. From the environment
|
|
68
|
-
* (options.env, else process.env, as the routes read it) it reads
|
|
69
|
-
* ceiling, which no study file can know. Each route's
|
|
70
|
-
* makes, in the route's order and with its codes and
|
|
71
|
-
* the same planner.
|
|
68
|
+
* (options.env, else process.env, as the routes read it) it reads the operator's E2B plan limits,
|
|
69
|
+
* the sandbox ceiling and the concurrent sandboxes, which no study file can know. Each route's
|
|
70
|
+
* planner makes every refusal that route makes, in the route's order and with its codes and
|
|
71
|
+
* messages; the route's exported runner calls the same planner.
|
|
72
72
|
*/
|
|
73
73
|
export function planStudy(config, options, deps = {}) {
|
|
74
74
|
const study = localBrowserDefaults(config);
|
|
75
75
|
const input = { dryRun: resolveStudyDryRun(study, options.dryRun, true) ?? true };
|
|
76
|
-
const
|
|
77
|
-
const
|
|
76
|
+
const env = options.env ?? process.env;
|
|
77
|
+
const e2b = {
|
|
78
|
+
sandboxCeiling: sandboxCeiling(env),
|
|
79
|
+
concurrentSandboxes: concurrentSandboxes(env),
|
|
80
|
+
};
|
|
81
|
+
const result = planRoute(routeOf(config), study, options, input, deps, e2b);
|
|
78
82
|
if (!result.ok)
|
|
79
83
|
return result;
|
|
80
84
|
// The manifest the CLI resolved enters the plan here and nowhere else; the routes read
|
|
81
85
|
// plan.study for the run's status record and bundle, and plan.warnings for its bundle events.
|
|
82
|
-
const warnings = recordedStudyWarnings(study);
|
|
86
|
+
const warnings = [...recordedStudyWarnings(study), ...(result.plan.warnings ?? [])];
|
|
83
87
|
const plan = {
|
|
84
88
|
...result.plan,
|
|
85
89
|
...(options.study === undefined ? {} : { study: options.study }),
|
|
@@ -87,7 +91,8 @@ export function planStudy(config, options, deps = {}) {
|
|
|
87
91
|
};
|
|
88
92
|
return { ok: true, planned: { plan } };
|
|
89
93
|
}
|
|
90
|
-
function planRoute(route, study, options, input, deps,
|
|
94
|
+
function planRoute(route, study, options, input, deps, e2b) {
|
|
95
|
+
const { sandboxCeiling } = e2b;
|
|
91
96
|
switch (route) {
|
|
92
97
|
case "preview":
|
|
93
98
|
return planPreview(study, options, input);
|
|
@@ -98,13 +103,13 @@ function planRoute(route, study, options, input, deps, sandboxCeiling) {
|
|
|
98
103
|
driving: callerDrivingOf(options),
|
|
99
104
|
...(options.count === undefined ? {} : { countOverride: options.count }),
|
|
100
105
|
...(options.rerun === undefined ? {} : { rerun: options.rerun }),
|
|
101
|
-
|
|
106
|
+
...e2b,
|
|
102
107
|
});
|
|
103
108
|
case "shared-world":
|
|
104
109
|
return planSharedWorldStudy(study, {
|
|
105
110
|
...input,
|
|
106
111
|
hasRunSession: deps.runSession !== undefined,
|
|
107
|
-
|
|
112
|
+
...e2b,
|
|
108
113
|
});
|
|
109
114
|
case "terminal":
|
|
110
115
|
return planTerminalStudy(study, {
|
|
@@ -113,6 +118,10 @@ function planRoute(route, study, options, input, deps, sandboxCeiling) {
|
|
|
113
118
|
sandboxCeiling,
|
|
114
119
|
});
|
|
115
120
|
case "scripted":
|
|
116
|
-
return planScriptedStudy(study, {
|
|
121
|
+
return planScriptedStudy(study, {
|
|
122
|
+
...input,
|
|
123
|
+
injectedBrowser: injectedBrowser(deps),
|
|
124
|
+
sandboxCeiling,
|
|
125
|
+
});
|
|
117
126
|
}
|
|
118
127
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// The preflight result a run returns, built from its context. preflight.ts and the probes in
|
|
2
2
|
// preflight-probes.ts both finish through these, so they live below both.
|
|
3
|
-
import {
|
|
3
|
+
import { studyAnalysisBudget } from "./automatic-analysis-plan.js";
|
|
4
4
|
import { digestText } from "../evidence/redaction.js";
|
|
5
5
|
import { computerUseParticipantCount } from "./routing.js";
|
|
6
6
|
/** The schema of `study check` results: whether a study is ready to run on this machine. */
|
|
7
7
|
export const STUDY_CHECK_SCHEMA = "humanish.study-check.v1";
|
|
8
8
|
export function finalize(ctx, args) {
|
|
9
9
|
const checks = [...ctx.checks, ...(args?.check ? [args.check] : []), ...(args?.checks ?? [])];
|
|
10
|
-
const analysis =
|
|
10
|
+
const analysis = studyAnalysisBudget(ctx.config.review?.analysis, ctx.route, computerUseParticipantCount(ctx.config));
|
|
11
11
|
return {
|
|
12
12
|
schema: STUDY_CHECK_SCHEMA,
|
|
13
13
|
...(analysis ? { analysis } : {}),
|
|
@@ -3,6 +3,7 @@ import type { E2BDesktopModule } from "../substrates/e2b/sdk.js";
|
|
|
3
3
|
import { type StudyConfig } from "./types.js";
|
|
4
4
|
import { STUDY_CHECK_SCHEMA } from "./preflight-result.js";
|
|
5
5
|
import { type StudyRoute } from "./plan.js";
|
|
6
|
+
import type { PlanRefusal } from "./plan-types.js";
|
|
6
7
|
import { type StudyResolveFailure } from "./discover.js";
|
|
7
8
|
export type StudyPreflightReachabilityMode = "metadata" | "public-preview" | "sandbox-loopback" | "prepared-host";
|
|
8
9
|
export interface StudyPreflightCheck {
|
|
@@ -57,7 +58,7 @@ export interface StudyPreflightResult {
|
|
|
57
58
|
spend: StudyPreflightSpend;
|
|
58
59
|
warnings: string[];
|
|
59
60
|
error?: {
|
|
60
|
-
code: StudyResolveFailure["error"]["code"] | "HUMANISH_STUDY_PREFLIGHT_INVALID_OPTION" | "HUMANISH_STUDY_PREFLIGHT_UNSUPPORTED_ROUTE" | "HUMANISH_STUDY_PREFLIGHT_TARGET_POLICY" | "HUMANISH_STUDY_PREFLIGHT_ENV_MISSING" | "HUMANISH_STUDY_PREFLIGHT_E2B_REQUIRED" | "HUMANISH_STUDY_PREFLIGHT_TARGET_UNREACHABLE" | "HUMANISH_STUDY_PREFLIGHT_PROVISION_FAILED" | "HUMANISH_STUDY_PREFLIGHT_TEARDOWN_FAILED";
|
|
61
|
+
code: StudyResolveFailure["error"]["code"] | "HUMANISH_STUDY_PREFLIGHT_INVALID_OPTION" | "HUMANISH_STUDY_PREFLIGHT_UNSUPPORTED_ROUTE" | "HUMANISH_STUDY_PREFLIGHT_TARGET_POLICY" | "HUMANISH_STUDY_PREFLIGHT_ENV_MISSING" | "HUMANISH_STUDY_PREFLIGHT_E2B_REQUIRED" | "HUMANISH_STUDY_PREFLIGHT_TARGET_UNREACHABLE" | "HUMANISH_STUDY_PREFLIGHT_PROVISION_FAILED" | "HUMANISH_STUDY_PREFLIGHT_TEARDOWN_FAILED" | PlanRefusal["code"];
|
|
61
62
|
message: string;
|
|
62
63
|
};
|
|
63
64
|
}
|
package/dist/study/preflight.js
CHANGED
|
@@ -4,8 +4,8 @@ import path from "node:path";
|
|
|
4
4
|
import { isLoopbackUrl } from "./parse/subject.js";
|
|
5
5
|
import {} from "./types.js";
|
|
6
6
|
import { runPublicPreviewPreflight, runSandboxLoopbackPreflight } from "./preflight-probes.js";
|
|
7
|
-
import { digest, finalize, STUDY_CHECK_SCHEMA } from "./preflight-result.js";
|
|
8
|
-
import { routeOf } from "./plan.js";
|
|
7
|
+
import { digest, fail, finalize, STUDY_CHECK_SCHEMA } from "./preflight-result.js";
|
|
8
|
+
import { planStudy, routeOf } from "./plan.js";
|
|
9
9
|
import { resolveStudyManifest } from "./discover.js";
|
|
10
10
|
import { participantList } from "./study-fields.js";
|
|
11
11
|
import { isLocalBrowserStudy } from "../substrates/local/runtime-config.js";
|
|
@@ -42,6 +42,7 @@ export async function runStudyPreflight(options) {
|
|
|
42
42
|
? new Map()
|
|
43
43
|
: (await resolveCommittedPersonasForCwd(cwd, studyPersonaIds(resolved.config))).personas;
|
|
44
44
|
const route = routeOf(resolved.config);
|
|
45
|
+
const env = options.env ?? process.env;
|
|
45
46
|
const ctx = {
|
|
46
47
|
cwd,
|
|
47
48
|
study: options.study,
|
|
@@ -52,7 +53,7 @@ export async function runStudyPreflight(options) {
|
|
|
52
53
|
route,
|
|
53
54
|
reachability,
|
|
54
55
|
timeoutMs,
|
|
55
|
-
env
|
|
56
|
+
env,
|
|
56
57
|
hooks: options.hooks ?? {},
|
|
57
58
|
checks: [
|
|
58
59
|
{ name: "study file", ok: true, message: `resolved ${resolved.origin} study file` },
|
|
@@ -65,6 +66,14 @@ export async function runStudyPreflight(options) {
|
|
|
65
66
|
...personaBackgroundWarnings(resolved.config.id, participants, personas),
|
|
66
67
|
],
|
|
67
68
|
};
|
|
69
|
+
// The plan `humanish run <study>` makes before it creates anything: no run options, so the file's
|
|
70
|
+
// mode decides dry or live, and the same environment for the sandbox ceiling. A study the run
|
|
71
|
+
// would refuse fails here with the run's code, before any probe creates a sandbox.
|
|
72
|
+
const planned = planStudy(resolved.config, { cwd, env });
|
|
73
|
+
if (!planned.ok) {
|
|
74
|
+
const { code, message } = planned.refusal;
|
|
75
|
+
return fail(ctx, code, message, [{ name: "plan", ok: false, message }]);
|
|
76
|
+
}
|
|
68
77
|
const machine = machineCheck(ctx.config);
|
|
69
78
|
// A local study's app and desktops run on this machine, so the hosted probes do not apply.
|
|
70
79
|
if (reachability !== "metadata" && isLocalBrowserStudy(ctx.config))
|
package/dist/study/routing.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { StudyConfig } from "./types.js";
|
|
2
|
-
|
|
2
|
+
/** Why a study of `count` participants cannot run, or undefined when it can. */
|
|
3
|
+
export declare function participantCountReason(count: number): string | undefined;
|
|
3
4
|
export declare function actorResolvesToComputerUse(type: string | undefined): boolean;
|
|
4
5
|
export declare function registeredComputerUseActors(): string[];
|
|
5
6
|
export declare function actorResolvesToScriptedBrowser(type: string | undefined): boolean;
|
|
@@ -9,11 +10,11 @@ export declare function registeredScriptedBrowserActors(): string[];
|
|
|
9
10
|
export declare function actorResolvesToTerminal(type: string | undefined): boolean;
|
|
10
11
|
export declare function registeredTerminalActors(): string[];
|
|
11
12
|
/**
|
|
12
|
-
* The
|
|
13
|
-
*
|
|
13
|
+
* The fan-out participant count on the computer-use route: a `participants` list's length, else
|
|
14
|
+
* the caller's count override, else a participant count, else 1. The single source of truth shared by the parser, the engine,
|
|
14
15
|
* and the pre-flight plan so the participant count is computed the same way everywhere.
|
|
15
16
|
*/
|
|
16
|
-
export declare function computerUseParticipantCount(config: StudyConfig): number;
|
|
17
|
+
export declare function computerUseParticipantCount(config: StudyConfig, countOverride?: number): number;
|
|
17
18
|
/**
|
|
18
19
|
* A participant's id: its declared roster id, else `lane-NN` (independent participants) or `role-NN`
|
|
19
20
|
* (shared-world participants) from its 0-based position. The parser's filled email recipients and
|
package/dist/study/routing.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { actorRegistry } from "../actors/registry.js";
|
|
2
2
|
import { isLoopbackUrl } from "./parse/subject.js";
|
|
3
3
|
import { participantList, declaredParticipantCount } from "./study-fields.js";
|
|
4
|
-
//
|
|
5
|
-
// and
|
|
6
|
-
|
|
4
|
+
// The most participants one study holds, on any route. One run keeps every participant's stream in
|
|
5
|
+
// one bundle, one Observer view and one analysis, and those are sized for 100: the analysis reads a
|
|
6
|
+
// bundle of at most 128 streams. How many run at once is a separate limit, the E2B plan's
|
|
7
|
+
// (concurrentSandboxes in substrates/e2b/lifetime.ts), and caps.maxUsd bounds what they spend.
|
|
8
|
+
const MAX_STUDY_PARTICIPANTS = 100;
|
|
9
|
+
/** Why a study of `count` participants cannot run, or undefined when it can. */
|
|
10
|
+
export function participantCountReason(count) {
|
|
11
|
+
return count > MAX_STUDY_PARTICIPANTS
|
|
12
|
+
? `A study runs at most ${MAX_STUDY_PARTICIPANTS} participants, and this one has ${count}. One run keeps every participant's evidence in one bundle and one Observer view, and humanish supports runs of up to ${MAX_STUDY_PARTICIPANTS}; split the participants across studies.`
|
|
13
|
+
: undefined;
|
|
14
|
+
}
|
|
7
15
|
// Run kinds whose evidence is the participant's screenshots. An actor that declares it produces none
|
|
8
16
|
// cannot run on them: a code-only actor would claim a GUI flow it only reached through a shell.
|
|
9
17
|
const SCREENSHOT_RUN_KINDS = new Set([
|
|
@@ -46,12 +54,13 @@ export function registeredTerminalActors() {
|
|
|
46
54
|
return registeredActorsOn("terminal");
|
|
47
55
|
}
|
|
48
56
|
/**
|
|
49
|
-
* The
|
|
50
|
-
*
|
|
57
|
+
* The fan-out participant count on the computer-use route: a `participants` list's length, else
|
|
58
|
+
* the caller's count override, else a participant count, else 1. The single source of truth shared by the parser, the engine,
|
|
51
59
|
* and the pre-flight plan so the participant count is computed the same way everywhere.
|
|
52
60
|
*/
|
|
53
|
-
export function computerUseParticipantCount(config) {
|
|
54
|
-
return participantList(config)?.length ??
|
|
61
|
+
export function computerUseParticipantCount(config, countOverride) {
|
|
62
|
+
return (participantList(config)?.length ??
|
|
63
|
+
Math.max(1, countOverride ?? declaredParticipantCount(config) ?? 1));
|
|
55
64
|
}
|
|
56
65
|
/**
|
|
57
66
|
* A participant's id: its declared roster id, else `lane-NN` (independent participants) or `role-NN`
|
package/dist/study/summary.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type LocalRuntimeStatus } from "../substrates/local/runtime.js";
|
|
2
|
-
import { type
|
|
2
|
+
import { type StudyAnalysisBudget } from "./automatic-analysis-plan.js";
|
|
3
3
|
import { type KeyResolutionDeps } from "../keys/key-resolution.js";
|
|
4
4
|
export declare const STUDY_SUMMARY_SCHEMA = "humanish.study-summary.v1";
|
|
5
5
|
export interface StudyCaps {
|
|
@@ -15,8 +15,8 @@ export interface StudySummary {
|
|
|
15
15
|
message: string;
|
|
16
16
|
};
|
|
17
17
|
communications?: string;
|
|
18
|
-
analysis?:
|
|
19
|
-
/** The budget line `run` and `study check` print, from
|
|
18
|
+
analysis?: StudyAnalysisBudget & {
|
|
19
|
+
/** The budget line `run` and `study check` print, from formatStudyAnalysisBudget. */
|
|
20
20
|
line: string;
|
|
21
21
|
};
|
|
22
22
|
schema: typeof STUDY_SUMMARY_SCHEMA;
|
package/dist/study/summary.js
CHANGED
|
@@ -11,7 +11,7 @@ import { localRuntimeStatus } from "../substrates/local/runtime.js";
|
|
|
11
11
|
import { planStudy, resolveStudyDryRun, routeOf } from "./plan.js";
|
|
12
12
|
import { localCodexParticipantCheck } from "./doctor.js";
|
|
13
13
|
import { requiredKeys, requiredSubjectEnv } from "./requirements.js";
|
|
14
|
-
import {
|
|
14
|
+
import { formatStudyAnalysisBudget, studyAnalysisBudget, } from "./automatic-analysis-plan.js";
|
|
15
15
|
import { DEFAULT_OPENAI_CU_MODEL, DEFAULT_OPENAI_CU_REASONING_EFFORT, } from "../actors/computer-use/openai-provider.js";
|
|
16
16
|
import { inspectStudyManifest } from "./discover.js";
|
|
17
17
|
import { computerUseParticipantCount, isComputerUseComposition } from "./routing.js";
|
|
@@ -123,7 +123,7 @@ export async function readStudySummary(cwd, study, options = {}) {
|
|
|
123
123
|
missingKeys = missing;
|
|
124
124
|
}
|
|
125
125
|
// Computed once: a test-then-use pair reads as though the two calls could differ.
|
|
126
|
-
const analysis =
|
|
126
|
+
const analysis = studyAnalysisBudget(inspected.config.review?.analysis, route, computerUseParticipantCount(inspected.config));
|
|
127
127
|
const subject = subjectOf(config);
|
|
128
128
|
const participants = participantsOf(inspected.config);
|
|
129
129
|
const caps = summaryCapsOf(inspected.config);
|
|
@@ -140,9 +140,7 @@ export async function readStudySummary(cwd, study, options = {}) {
|
|
|
140
140
|
? await localCodexParticipantCheck({ env: options.env ?? process.env })
|
|
141
141
|
: undefined;
|
|
142
142
|
return {
|
|
143
|
-
...(analysis
|
|
144
|
-
? { analysis: { ...analysis, line: formatAutomaticAnalysisBudget(analysis) } }
|
|
145
|
-
: {}),
|
|
143
|
+
...(analysis ? { analysis: { ...analysis, line: formatStudyAnalysisBudget(analysis) } } : {}),
|
|
146
144
|
...(runtime
|
|
147
145
|
? { runtime: { ok: runtime.ok, installed: runtime.installed, message: runtime.message } }
|
|
148
146
|
: {}),
|
package/dist/study/types.d.ts
CHANGED
|
@@ -349,6 +349,20 @@ export interface StudyActor {
|
|
|
349
349
|
model?: string;
|
|
350
350
|
/** First-party OpenAI CUA only: per-response output limit including reasoning, not a dollar cap. */
|
|
351
351
|
maxOutputTokens?: number;
|
|
352
|
+
/**
|
|
353
|
+
* Computer-use and shared-world: the longest one `wait` action lasts, in whole milliseconds from
|
|
354
|
+
* 1000 to 600000. Default 120000, or 30000 on a desktop with speech. A longer wait is shortened
|
|
355
|
+
* to it, the trace records a `wait shortened` notice, and the participant is told on its next
|
|
356
|
+
* turn.
|
|
357
|
+
*/
|
|
358
|
+
maxWaitMs?: number;
|
|
359
|
+
/**
|
|
360
|
+
* Computer-use and shared-world: how long a `wait` that names no duration lasts when the
|
|
361
|
+
* participant's turn only waits or takes screenshots, in whole milliseconds from 1000 to 600000
|
|
362
|
+
* and no longer than maxWaitMs. Default 5000. After an action in the same turn such a wait lasts
|
|
363
|
+
* 500 ms. OpenAI's computer-use `wait` never names a duration.
|
|
364
|
+
*/
|
|
365
|
+
idleWaitMs?: number;
|
|
352
366
|
/**
|
|
353
367
|
* `local-agent` only: which locally signed-in coding agent is the brain. Absent = codex.
|
|
354
368
|
*
|
|
@@ -2,8 +2,8 @@ import type { StudyConfig } from "./types.js";
|
|
|
2
2
|
/**
|
|
3
3
|
* Cross-validate the computer-use fan-out declaration (`per-lane-worlds`). Returns the failure
|
|
4
4
|
* message, or null when valid. The parser enforces it, and runStudyWith checks it again for a config
|
|
5
|
-
* that skipped the parser. It runs rosterStructuralValidationReason (id and device
|
|
6
|
-
* validity, unique ids) first, then the route-scoped XOR,
|
|
5
|
+
* that skipped the parser. It runs rosterStructuralValidationReason (roster size, id and device
|
|
6
|
+
* validity, unique ids) first, then the route-scoped XOR, participant count and policy checks.
|
|
7
7
|
*/
|
|
8
8
|
export declare function computerUseValidationReason(config: StudyConfig): string | null;
|
|
9
9
|
/**
|
|
@@ -36,6 +36,11 @@ export declare function taskProtocolValidationReason(config: StudyConfig, suppor
|
|
|
36
36
|
export declare function cloneTargetValidationReason(config: StudyConfig): string | null;
|
|
37
37
|
/** Refuse a claimed output bound when the route cannot pass it to the first-party provider. */
|
|
38
38
|
export declare function outputTokenLimitValidationReason(config: StudyConfig): string | null;
|
|
39
|
+
/**
|
|
40
|
+
* Refuse a wait field out of range, or an idle wait longer than the longest wait. On a route whose
|
|
41
|
+
* participants take no wait actions the fields are inert (warnings.ts).
|
|
42
|
+
*/
|
|
43
|
+
export declare function waitLimitValidationReason(config: StudyConfig): string | null;
|
|
39
44
|
/**
|
|
40
45
|
* Cross-validate a concurrent shared-world declaration. Returns the failure message,
|
|
41
46
|
* or null when valid. Includes the base shared-world checks plus the concurrent extras: a synthetic
|
package/dist/study/validation.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { isMaxOutputTokens } from "../actors/output-token-limit.js";
|
|
2
|
+
import { MAX_RECEIVING_INBOXES } from "../comms/receiving-types.js";
|
|
3
|
+
import { waitFieldsReason } from "../actors/computer-use/wait.js";
|
|
2
4
|
import { PARTICIPANT_ID_MAX_CHARS, PARTICIPANT_ID_PATTERN } from "./parse/actors.js";
|
|
3
5
|
import { isHttpUrl, isLoopbackUrl } from "./parse/subject.js";
|
|
4
6
|
import { declaredTargets } from "./plan-participants.js";
|
|
5
|
-
import { actorResolvesToComputerUse, computerUseParticipantCount,
|
|
7
|
+
import { actorResolvesToComputerUse, computerUseParticipantCount, participantCountReason, registeredComputerUseActors, resolveEntryUrl, isComputerUseComposition, isScriptedBrowserComposition, isSharedWorldComposition, isTerminalProductComposition, } from "./routing.js";
|
|
6
8
|
import { participantList } from "./study-fields.js";
|
|
7
9
|
/**
|
|
8
10
|
* Cross-validate the computer-use fan-out declaration (`per-lane-worlds`). Returns the failure
|
|
9
11
|
* message, or null when valid. The parser enforces it, and runStudyWith checks it again for a config
|
|
10
|
-
* that skipped the parser. It runs rosterStructuralValidationReason (id and device
|
|
11
|
-
* validity, unique ids) first, then the route-scoped XOR,
|
|
12
|
+
* that skipped the parser. It runs rosterStructuralValidationReason (roster size, id and device
|
|
13
|
+
* validity, unique ids) first, then the route-scoped XOR, participant count and policy checks.
|
|
12
14
|
*/
|
|
13
15
|
export function computerUseValidationReason(config) {
|
|
14
16
|
const roster = participantList(config);
|
|
@@ -41,9 +43,9 @@ export function computerUseValidationReason(config) {
|
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
45
|
const participantCount = computerUseParticipantCount(config);
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
const countReason = participantCountReason(participantCount);
|
|
47
|
+
if (countReason)
|
|
48
|
+
return countReason;
|
|
47
49
|
// Public targets fan out into N independent worlds driving the same public app, which is an
|
|
48
50
|
// ambiguous shared-world-ish shape, not a per-participant target swarm. Permit N>1 public runs only
|
|
49
51
|
// when every roster entry declares its own target, making the adapter-owned topology explicit. But when
|
|
@@ -59,8 +61,8 @@ export function computerUseValidationReason(config) {
|
|
|
59
61
|
return null;
|
|
60
62
|
}
|
|
61
63
|
/**
|
|
62
|
-
* Engine-level
|
|
63
|
-
* public TypeScript/JavaScript API instead of parseStudy.
|
|
64
|
+
* Engine-level roster validation (its size and its ids' path tokens) for configs supplied directly
|
|
65
|
+
* through the public TypeScript/JavaScript API instead of parseStudy.
|
|
64
66
|
*/
|
|
65
67
|
function rosterStructuralValidationReason(config) {
|
|
66
68
|
const roster = participantList(config);
|
|
@@ -69,6 +71,9 @@ function rosterStructuralValidationReason(config) {
|
|
|
69
71
|
if (!Array.isArray(roster) || roster.length === 0) {
|
|
70
72
|
return "participants must be a non-empty array when set.";
|
|
71
73
|
}
|
|
74
|
+
const countReason = participantCountReason(roster.length);
|
|
75
|
+
if (countReason)
|
|
76
|
+
return countReason;
|
|
72
77
|
for (const [index, entry] of roster.entries()) {
|
|
73
78
|
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
74
79
|
return `participants[${index}] must be an object.`;
|
|
@@ -194,6 +199,10 @@ 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
|
}
|
|
202
|
+
const participants = computerUseParticipantCount(config);
|
|
203
|
+
if (participants > MAX_RECEIVING_INBOXES) {
|
|
204
|
+
return `Real email receiving leases one inbox per participant, at most ${MAX_RECEIVING_INBOXES} per run, and this study has ${participants} participants. Run at most ${MAX_RECEIVING_INBOXES} participants with a real inbox, or split them across studies.`;
|
|
205
|
+
}
|
|
197
206
|
if (config.actor?.type === "local-agent") {
|
|
198
207
|
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
208
|
}
|
|
@@ -235,16 +244,23 @@ export function outputTokenLimitValidationReason(config) {
|
|
|
235
244
|
}
|
|
236
245
|
return null;
|
|
237
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* Refuse a wait field out of range, or an idle wait longer than the longest wait. On a route whose
|
|
249
|
+
* participants take no wait actions the fields are inert (warnings.ts).
|
|
250
|
+
*/
|
|
251
|
+
export function waitLimitValidationReason(config) {
|
|
252
|
+
return config.actor === undefined ? null : waitFieldsReason(config.actor, "actor.");
|
|
253
|
+
}
|
|
238
254
|
/**
|
|
239
255
|
* Shared-world participants share one live app, so at least two must be live at once. The
|
|
240
|
-
* sequential shared-world route (`execution.concurrency: 1`) was removed in 0.106.0
|
|
241
|
-
*
|
|
242
|
-
* two-participant roster floor, so a one-participant roster gets the
|
|
243
|
-
* one.
|
|
256
|
+
* sequential shared-world route (`execution.concurrency: 1`) was removed in 0.106.0. An omitted
|
|
257
|
+
* concurrency runs every participant, up to the E2B plan's limit, which the planner checks. Both
|
|
258
|
+
* callers check it after their two-participant roster floor, so a one-participant roster gets the
|
|
259
|
+
* roster refusal, never this one.
|
|
244
260
|
*/
|
|
245
261
|
function sharedWorldConcurrencyReason(config) {
|
|
246
|
-
//
|
|
247
|
-
//
|
|
262
|
+
// An omitted value runs every participant, so it is checked as the participant count. A missing
|
|
263
|
+
// roster reads as 0 and is refused.
|
|
248
264
|
const participants = participantList(config)?.length ?? 0;
|
|
249
265
|
const concurrency = config.execution?.concurrency ?? participants;
|
|
250
266
|
if (concurrency >= 2)
|
package/dist/study/warnings.js
CHANGED
|
@@ -50,6 +50,18 @@ const ACTOR_ROWS = [
|
|
|
50
50
|
field: "actor.model",
|
|
51
51
|
applies: (config, routes) => otherRoute(routes) && Boolean(config.actor?.model),
|
|
52
52
|
},
|
|
53
|
+
// Only a computer-use loop dispatches wait actions; terminal, scripted and preview participants
|
|
54
|
+
// take none.
|
|
55
|
+
{
|
|
56
|
+
field: "actor.maxWaitMs",
|
|
57
|
+
reason: "the longest wait action of a computer-use participant; needs route: computer-use or shared-world",
|
|
58
|
+
applies: (config, routes) => config.actor?.maxWaitMs !== undefined && !routes.cua && !routes.shared,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
field: "actor.idleWaitMs",
|
|
62
|
+
reason: "how long a computer-use participant's wait with no duration lasts; needs route: computer-use or shared-world",
|
|
63
|
+
applies: (config, routes) => config.actor?.idleWaitMs !== undefined && !routes.cua && !routes.shared,
|
|
64
|
+
},
|
|
53
65
|
];
|
|
54
66
|
const TERMINAL_ONLY = "needs subject.source: terminal-product + a registered terminal actor";
|
|
55
67
|
const RESERVED = "reserved; no route reads it yet";
|
package/dist/subject/serve.d.ts
CHANGED
|
@@ -37,3 +37,6 @@ export declare function runSubjectServePipeline(shell: Shell, args: {
|
|
|
37
37
|
* an in-sandbox git refresh, because the archive excludes .git). */
|
|
38
38
|
onPhaseComplete?: () => Promise<void>;
|
|
39
39
|
} & DetachedTimers): Promise<void>;
|
|
40
|
+
/** A serve.build that exited non-zero or ran out of time; the message carries its redacted log tail. */
|
|
41
|
+
export declare class SubjectBuildError extends Error {
|
|
42
|
+
}
|
package/dist/subject/serve.js
CHANGED
|
@@ -196,6 +196,9 @@ async function installSubject(step, command) {
|
|
|
196
196
|
throw new Error(`${headline}: ${failureTail(step.scrub(install.logTail))}`);
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
|
+
/** A serve.build that exited non-zero or ran out of time; the message carries its redacted log tail. */
|
|
200
|
+
export class SubjectBuildError extends Error {
|
|
201
|
+
}
|
|
199
202
|
/** serve.build, once. */
|
|
200
203
|
async function buildSubject(step, command) {
|
|
201
204
|
const { now } = step;
|
|
@@ -211,7 +214,7 @@ async function buildSubject(step, command) {
|
|
|
211
214
|
});
|
|
212
215
|
emitPhaseCompleted(step.onPhase, now, buildStartedAt, "build", build.ok, build.ok ? "subject build complete" : "subject build failed");
|
|
213
216
|
if (!build.ok) {
|
|
214
|
-
throw new
|
|
217
|
+
throw new SubjectBuildError(`subject build ${build.timedOut ? "timed out" : `failed (exit ${build.exitCode})`}: ${failureTail(step.scrub(build.logTail))}`);
|
|
215
218
|
}
|
|
216
219
|
}
|
|
217
220
|
/** serve.start as a detached process that keeps running. */
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
//
|
|
4
4
|
// - Scripts are written via `writeFile`, never heredocs, which eliminates the
|
|
5
5
|
// sentinel-collision bug class (a command line that equals the heredoc terminator) by
|
|
6
|
-
// construction.
|
|
6
|
+
// construction. The author's command is a script file of its own, so bash parses it apart
|
|
7
|
+
// from the wrapper: a syntax error in it is that script's exit 2, logged, and the wrapper
|
|
8
|
+
// still writes the status.
|
|
7
9
|
// - Bounded steps (install/build) run detached with an atomically-written status file
|
|
8
10
|
// (write tmp + mv), polled by short foreground commands; a timeout kills the process
|
|
9
11
|
// group and surfaces a capped log tail for the caller to redact and persist.
|
|
@@ -34,10 +36,11 @@ function assertName(name) {
|
|
|
34
36
|
function stepDir(name) {
|
|
35
37
|
return `${WORK_ROOT}/${name}`;
|
|
36
38
|
}
|
|
37
|
-
// The wrapper script: runs the command from its own session (Shell.start makes the
|
|
38
|
-
// process-group leader, so `kill -- -PID` reclaims the whole tree), logs everything,
|
|
39
|
-
// writes the exit code atomically so a poller can never read a half-written status.
|
|
40
|
-
|
|
39
|
+
// The wrapper script: runs the command script from its own session (Shell.start makes the
|
|
40
|
+
// wrapper the process-group leader, so `kill -- -PID` reclaims the whole tree), logs everything,
|
|
41
|
+
// and writes the exit code atomically so a poller can never read a half-written status. It holds
|
|
42
|
+
// no author text, so it always parses and always reaches the status write.
|
|
43
|
+
function wrapperScript(name, cwd) {
|
|
41
44
|
const dir = stepDir(name);
|
|
42
45
|
return [
|
|
43
46
|
"#!/bin/bash",
|
|
@@ -46,7 +49,7 @@ function wrapperScript(name, command, cwd) {
|
|
|
46
49
|
cwd === undefined
|
|
47
50
|
? ": # no cwd override"
|
|
48
51
|
: `cd ${shellQuote(cwd)} || { echo 127 > ${shellQuote(`${dir}/status.tmp`)}; mv ${shellQuote(`${dir}/status.tmp`)} ${shellQuote(`${dir}/status`)}; exit 127; }`,
|
|
49
|
-
`
|
|
52
|
+
`bash ${shellQuote(`${dir}/command.sh`)} > ${shellQuote(`${dir}/log.txt`)} 2>&1`,
|
|
50
53
|
"code=$?",
|
|
51
54
|
`echo $code > ${shellQuote(`${dir}/status.tmp`)}`,
|
|
52
55
|
`mv ${shellQuote(`${dir}/status.tmp`)} ${shellQuote(`${dir}/status`)}`,
|
|
@@ -59,7 +62,11 @@ async function writeAndLaunch(shell, name, command, cwd, requestTimeoutMs) {
|
|
|
59
62
|
const dir = stepDir(name);
|
|
60
63
|
const scriptPath = `${dir}/run.sh`;
|
|
61
64
|
await runOrThrow(shell, `mkdir -p ${shellQuote(dir)}`, { requestTimeoutMs });
|
|
62
|
-
|
|
65
|
+
// The study parser trims each command, so a YAML `|` block arrives without its final newline.
|
|
66
|
+
// bash closes a heredoc whose delimiter is a script's last line either way; the newline makes
|
|
67
|
+
// the file plain text.
|
|
68
|
+
await shell.writeFile(`${dir}/command.sh`, `${command}\n`);
|
|
69
|
+
await shell.writeFile(scriptPath, wrapperScript(name, cwd));
|
|
63
70
|
throwOnExit(await shell.start(`bash ${shellQuote(scriptPath)}`, { requestTimeoutMs }));
|
|
64
71
|
}
|
|
65
72
|
/** Read the capped log tail for a step (raw; caller redacts). */
|
|
@@ -54,8 +54,6 @@ export interface E2BDesktopLike {
|
|
|
54
54
|
wait(ms: number): Promise<void> | void;
|
|
55
55
|
}
|
|
56
56
|
export interface E2BDesktopExecutorOptions {
|
|
57
|
-
/** Fallback wait when a wait action carries no ms. Default 500. */
|
|
58
|
-
defaultWaitMs?: number;
|
|
59
57
|
/**
|
|
60
58
|
* Pixels of CuaAction scroll dy per one SDK scroll tick. The executor maps
|
|
61
59
|
* abs(dy) / scrollAmountPerTick to the SDK's integer `amount` (floored at 1 for
|