humanish 0.110.3 → 0.111.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/CONTEXT.md +2 -2
- package/dist/actors/local-agent/claude-session.js +1 -1
- package/dist/actors/local-agent/cli.js +1 -1
- package/dist/actors/scripted-browser/actor.d.ts +2 -2
- package/dist/actors/scripted-browser/actor.js +17 -8
- package/dist/actors/scripted-browser/launch-env.d.ts +12 -0
- package/dist/actors/scripted-browser/launch-env.js +50 -0
- package/dist/analysis/automatic-completion.d.ts +2 -1
- package/dist/analysis/automatic-completion.js +7 -2
- package/dist/cli/commands/migrate.js +1 -1
- package/dist/cli/commands/runs.js +5 -2
- package/dist/cli/commands/study-route-computer-use.js +3 -2
- package/dist/cli/commands/study-route-preview.js +2 -1
- package/dist/cli/commands/study-route-shared-world.js +3 -3
- package/dist/cli/commands/study.js +2 -1
- package/dist/routes/computer-use/live-phase.js +8 -1
- package/dist/routes/computer-use/local-vm.js +2 -1
- package/dist/routes/computer-use/participant-model.js +1 -1
- package/dist/routes/computer-use/plan.js +9 -10
- package/dist/routes/computer-use/preflight.js +1 -1
- package/dist/routes/computer-use/result.js +1 -1
- package/dist/routes/computer-use/route.js +3 -2
- package/dist/routes/computer-use/setup.js +2 -1
- package/dist/routes/computer-use/types.d.ts +4 -4
- package/dist/routes/scripted/plan.js +5 -4
- package/dist/routes/scripted/route.js +6 -2
- package/dist/routes/scripted/scenario.d.ts +1 -1
- package/dist/routes/scripted/scenario.js +3 -3
- package/dist/routes/scripted/surface-sessions.d.ts +2 -0
- package/dist/routes/scripted/surface-sessions.js +2 -1
- package/dist/routes/scripted/types.d.ts +2 -2
- package/dist/routes/shared-world/handoff.js +1 -1
- package/dist/routes/shared-world/participant-specs.js +6 -2
- package/dist/routes/shared-world/plan.js +8 -8
- package/dist/routes/shared-world/route.js +3 -3
- package/dist/routes/shared-world/types.d.ts +1 -1
- package/dist/routes/terminal/bundle.d.ts +3 -3
- package/dist/routes/terminal/ledger.d.ts +2 -2
- package/dist/routes/terminal/lifetime.js +1 -1
- package/dist/routes/terminal/live-finish.js +1 -1
- package/dist/routes/terminal/plan.js +4 -3
- package/dist/routes/terminal/route.js +3 -2
- package/dist/routes/terminal/session.js +1 -0
- package/dist/routes/terminal/types.d.ts +1 -1
- package/dist/run/display.d.ts +10 -0
- package/dist/run/display.js +22 -0
- package/dist/run/reclaim.d.ts +4 -2
- package/dist/run/reclaim.js +61 -18
- package/dist/run/results.d.ts +1 -1
- package/dist/run/run.d.ts +11 -1
- package/dist/run/run.js +24 -5
- package/dist/run/status.d.ts +6 -0
- package/dist/run/status.js +1 -0
- package/dist/run/stored-runs.js +26 -11
- package/dist/run-study.d.ts +5 -3
- package/dist/run-study.js +7 -11
- package/dist/study/adapter-scorer-loader.d.ts +1 -1
- package/dist/study/adapter-scorer-loader.js +3 -3
- package/dist/study/composition-rules.js +49 -39
- package/dist/study/config.d.ts +31 -8
- package/dist/study/config.js +181 -155
- package/dist/study/discover.d.ts +1 -1
- package/dist/study/discover.js +1 -1
- package/dist/study/doctor.js +5 -4
- package/dist/study/keys.d.ts +250 -2
- package/dist/study/keys.js +32 -62
- package/dist/study/{convert.d.ts → migrate/convert.d.ts} +1 -1
- package/dist/study/{convert.js → migrate/convert.js} +28 -31
- package/dist/study/{migrate.d.ts → migrate/migrate.d.ts} +1 -1
- package/dist/study/{migrate.js → migrate/migrate.js} +2 -2
- package/dist/study/{owned-files.js → migrate/owned-files.js} +1 -1
- package/dist/study/migrate/v2.d.ts +78 -0
- package/dist/study/migrate/v2.js +607 -0
- package/dist/study/parse/actors.d.ts +34 -10
- package/dist/study/parse/actors.js +76 -174
- package/dist/study/parse/comms.js +6 -2
- package/dist/study/parse/execution.d.ts +9 -4
- package/dist/study/parse/execution.js +6 -41
- package/dist/study/parse/front.d.ts +36 -0
- package/dist/study/parse/{study-v3.js → front.js} +37 -88
- package/dist/study/parse/subject.d.ts +2 -0
- package/dist/study/parse/subject.js +15 -18
- package/dist/study/persona-resolve.d.ts +2 -4
- package/dist/study/persona-resolve.js +7 -8
- package/dist/study/plan-base.d.ts +1 -1
- package/dist/study/plan-base.js +6 -6
- package/dist/study/plan-participants.d.ts +2 -2
- package/dist/study/plan-participants.js +12 -11
- package/dist/study/plan-types.d.ts +5 -5
- package/dist/study/plan.js +5 -4
- package/dist/study/preflight-probes.js +4 -4
- package/dist/study/preflight.d.ts +1 -1
- package/dist/study/preflight.js +3 -3
- package/dist/study/routing.d.ts +5 -5
- package/dist/study/routing.js +17 -20
- package/dist/study/run-study-options.d.ts +1 -1
- package/dist/study/run-study-options.js +3 -3
- package/dist/study/study-deps.d.ts +1 -1
- package/dist/study/study-fields.d.ts +19 -0
- package/dist/study/study-fields.js +46 -0
- package/dist/study/summary.js +19 -18
- package/dist/study/types.d.ts +66 -104
- package/dist/study/types.js +0 -1
- package/dist/study/url-credentials.js +3 -3
- package/dist/study/validation.d.ts +4 -10
- package/dist/study/validation.js +54 -79
- package/dist/study/warnings.d.ts +3 -3
- package/dist/study/warnings.js +42 -110
- package/dist/substrates/local/runtime-config.js +14 -16
- package/dist/tui-app.js +3 -3
- package/docs/contracts/schemas.md +36 -8
- package/package.json +1 -1
- package/dist/study/parse/study-v3.d.ts +0 -21
- /package/dist/study/{owned-files.d.ts → migrate/owned-files.d.ts} +0 -0
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
//
|
|
7
7
|
// Spend: scripted steps make no model requests, and their traces record zero token usage. A local
|
|
8
8
|
// app-url run spends no sandbox minutes; a live clone run spends E2B minutes to clone and serve the
|
|
9
|
-
// subject. `
|
|
9
|
+
// subject. `mode: live` is required anyway, because a live run actuates a real browser
|
|
10
10
|
// against a real app (it fills forms and clicks buttons). A dry run parses and digest-pins the
|
|
11
11
|
// scenario and writes the contract bundle without touching anything.
|
|
12
12
|
//
|
|
@@ -31,6 +31,7 @@ import { ScriptedSubject } from "./subject.js";
|
|
|
31
31
|
import { runScriptedSessions, writeSurfaceTraces } from "./surface-sessions.js";
|
|
32
32
|
import {} from "./types.js";
|
|
33
33
|
import { studyResultIdentity } from "../../run/study-result.js";
|
|
34
|
+
import { actorOf } from "../../study/study-fields.js";
|
|
34
35
|
/**
|
|
35
36
|
* A refused scripted study's result, at the refusal's stage: a before-scope refusal has its own field
|
|
36
37
|
* order and no analysis record; the others come after the output directory checks.
|
|
@@ -38,7 +39,7 @@ import { studyResultIdentity } from "../../run/study-result.js";
|
|
|
38
39
|
export async function scriptedStudyRefusal(options, refusal) {
|
|
39
40
|
const { config, dryRun } = options;
|
|
40
41
|
const cwd = path.resolve(options.cwd);
|
|
41
|
-
const actorType = config
|
|
42
|
+
const actorType = actorOf(config)?.type ?? "";
|
|
42
43
|
if (refusal.beforeScope)
|
|
43
44
|
return {
|
|
44
45
|
...studyResultIdentity("scripted", config.id),
|
|
@@ -94,6 +95,8 @@ async function runScriptedPlanInScope(plan, input, scope) {
|
|
|
94
95
|
mintRunId: makeScriptedRunId,
|
|
95
96
|
mode: dryRun ? "dry-run" : "live",
|
|
96
97
|
study: plan.study,
|
|
98
|
+
// Only a clone subject is served from a sandbox; an app-url run drives a local browser.
|
|
99
|
+
sandboxes: setup.clone ? undefined : "none",
|
|
97
100
|
renderReview: renderScriptedReviewMarkdown,
|
|
98
101
|
observer: { open: input.open === true, render: setup.deps.renderObserver },
|
|
99
102
|
});
|
|
@@ -144,6 +147,7 @@ async function runScriptedPlanInScope(plan, input, scope) {
|
|
|
144
147
|
deps: setup.deps,
|
|
145
148
|
browserCommand: setup.browserCommand,
|
|
146
149
|
runPaths,
|
|
150
|
+
onSessionStart: () => run.participantStarted(),
|
|
147
151
|
});
|
|
148
152
|
}
|
|
149
153
|
catch (error) {
|
|
@@ -8,7 +8,7 @@ interface ResolvedScriptedScenario {
|
|
|
8
8
|
sourceDigest: string;
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
|
-
* Resolve and consume `scenario
|
|
11
|
+
* Resolve and consume `scenario`. Path-style refs (contain a separator or end .yaml/.yml)
|
|
12
12
|
* resolve against cwd and are clamped inside it: a ../../ escape is rejected, never recorded
|
|
13
13
|
* as repo-relative provenance. Id-style refs must be public-safe tokens and resolve to
|
|
14
14
|
* humanish/scenarios/<ref>.yaml (then .yml). Every failure mode is fail-closed. planScriptedStudy
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Resolve a scripted-browser study's `scenario
|
|
1
|
+
// Resolve a scripted-browser study's `scenario` to a committed scenario file and parse its
|
|
2
2
|
// `browser.steps` into the journey the actor runs. The ref is an id or a repo-relative path that must
|
|
3
3
|
// stay inside the project; provenance records it repo-relative with the scenario's digest.
|
|
4
4
|
import { parse as parseYaml } from "yaml";
|
|
@@ -6,11 +6,11 @@ import path from "node:path";
|
|
|
6
6
|
import { parseBrowserPersonaJourneyFromScenario } from "../../actors/scripted-browser/journey.js";
|
|
7
7
|
import { digestText } from "../../evidence/redaction.js";
|
|
8
8
|
import { readContainedRegularFile, } from "../../run/contained-output.js";
|
|
9
|
-
// Same public-safe token shape the study id uses; an id-style scenario
|
|
9
|
+
// Same public-safe token shape the study id uses; an id-style `scenario` must match it before
|
|
10
10
|
// it is interpolated into a repo path.
|
|
11
11
|
const SCENARIO_REF_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
12
12
|
/**
|
|
13
|
-
* Resolve and consume `scenario
|
|
13
|
+
* Resolve and consume `scenario`. Path-style refs (contain a separator or end .yaml/.yml)
|
|
14
14
|
* resolve against cwd and are clamped inside it: a ../../ escape is rejected, never recorded
|
|
15
15
|
* as repo-relative provenance. Id-style refs must be public-safe tokens and resolve to
|
|
16
16
|
* humanish/scenarios/<ref>.yaml (then .yml). Every failure mode is fail-closed. planScriptedStudy
|
|
@@ -8,6 +8,8 @@ export declare function runScriptedSessions(session: Required<Pick<ScriptedBrows
|
|
|
8
8
|
deps: StudyDeps;
|
|
9
9
|
browserCommand: string | undefined;
|
|
10
10
|
runPaths: PreparedRunArtifactPaths;
|
|
11
|
+
/** Called as each surface's session starts, before it can throw or return. */
|
|
12
|
+
onSessionStart: () => void;
|
|
11
13
|
}): Promise<ScriptedBrowserSessionResult[]>;
|
|
12
14
|
/** Writes each surface's actor-<surface>.json, as the computer-use route writes actor.json. */
|
|
13
15
|
export declare function writeSurfaceTraces(runPaths: PreparedRunArtifactPaths, results: readonly ScriptedBrowserSessionResult[]): Promise<void>;
|
|
@@ -7,9 +7,10 @@ import { validateScriptedSessionResult } from "./session-result.js";
|
|
|
7
7
|
/** One session per surface, in parallel. */
|
|
8
8
|
export function runScriptedSessions(session, run) {
|
|
9
9
|
const { appUrl, evidenceAppUrl, urlPolicy, journey, persona, timeoutMs, artifactRoot } = session;
|
|
10
|
-
const { surfaces, deps, browserCommand, runPaths } = run;
|
|
10
|
+
const { surfaces, deps, browserCommand, runPaths, onSessionStart } = run;
|
|
11
11
|
const runSession = deps.runScriptedSession;
|
|
12
12
|
return Promise.all(surfaces.map((surface) => {
|
|
13
|
+
onSessionStart();
|
|
13
14
|
const sessionOptions = {
|
|
14
15
|
appUrl,
|
|
15
16
|
evidenceAppUrl,
|
|
@@ -47,7 +47,7 @@ export interface ScriptedBrowserStudyResult extends AutomaticAnalysisResult, Stu
|
|
|
47
47
|
killed: boolean;
|
|
48
48
|
};
|
|
49
49
|
hostDigest?: string;
|
|
50
|
-
/** The consumed scenario
|
|
50
|
+
/** The consumed `scenario`: digest-pinned provenance of the executable steps. */
|
|
51
51
|
scenario?: {
|
|
52
52
|
id: string;
|
|
53
53
|
source: string;
|
|
@@ -58,7 +58,7 @@ export interface ScriptedBrowserStudyResult extends AutomaticAnalysisResult, Stu
|
|
|
58
58
|
observer?: ObserverResult;
|
|
59
59
|
warnings: string[];
|
|
60
60
|
error?: {
|
|
61
|
-
code: "HUMANISH_STUDY_ANALYSIS_INVALID" | "HUMANISH_STUDY_TASKS_UNSUPPORTED" | "HUMANISH_STUDY_OPTION_UNSUPPORTED" | "HUMANISH_STUDY_V2_UNSUPPORTED" | "HUMANISH_SCRIPTED_FAILED" | "HUMANISH_SCRIPTED_ACTOR_UNSUPPORTED" | "HUMANISH_SCRIPTED_SCENARIO_INVALID" | "HUMANISH_SCRIPTED_SUBJECT_UNSAFE" | "HUMANISH_SCRIPTED_BROWSER_MISSING" | "HUMANISH_SCRIPTED_KEYS_MISSING" | "HUMANISH_SCRIPTED_SUBJECT_ENV_MISSING" | "HUMANISH_SCRIPTED_GETHOST_UNAVAILABLE" | "HUMANISH_RUN_ID_IN_USE";
|
|
61
|
+
code: "HUMANISH_STUDY_ANALYSIS_INVALID" | "HUMANISH_STUDY_TASKS_UNSUPPORTED" | "HUMANISH_STUDY_OPTION_UNSUPPORTED" | "HUMANISH_STUDY_V2_UNSUPPORTED" | "HUMANISH_STUDY_INVALID" | "HUMANISH_SCRIPTED_FAILED" | "HUMANISH_SCRIPTED_ACTOR_UNSUPPORTED" | "HUMANISH_SCRIPTED_SCENARIO_INVALID" | "HUMANISH_SCRIPTED_SUBJECT_UNSAFE" | "HUMANISH_SCRIPTED_BROWSER_MISSING" | "HUMANISH_SCRIPTED_KEYS_MISSING" | "HUMANISH_SCRIPTED_SUBJECT_ENV_MISSING" | "HUMANISH_SCRIPTED_GETHOST_UNAVAILABLE" | "HUMANISH_RUN_ID_IN_USE";
|
|
62
62
|
message: string;
|
|
63
63
|
};
|
|
64
64
|
}
|
|
@@ -30,7 +30,7 @@ const HANDOFF_DEADLINE_BUDGET_FRACTION = 0.4;
|
|
|
30
30
|
// convergence proof): at most this many single-frame reads before the participant is assumed to be
|
|
31
31
|
// somewhere without a code. Each reader stops the instant it has what it needs, so in practice only
|
|
32
32
|
// a handful fire (a participant reaches its /lobby within a few turns). NOTE: these reads are
|
|
33
|
-
// out-of-band OpenAI calls (external-public route only) and are left out of
|
|
33
|
+
// out-of-band OpenAI calls (external-public route only) and are left out of caps.maxUsd,
|
|
34
34
|
// so this hard cap is what bounds their spend (each read is one cheap single-frame OCR call). If
|
|
35
35
|
// this route ever runs under a strict budget, fold the estimate in.
|
|
36
36
|
const MAX_LOBBY_CODE_VISION_READS = 30;
|
|
@@ -167,7 +167,11 @@ export function participantRunDeps(ctx, live, scrubKnownValues) {
|
|
|
167
167
|
studyCwd: ctx.cwd,
|
|
168
168
|
redactScreenshots: ctx.redactScreenshots,
|
|
169
169
|
scrubKnownValues,
|
|
170
|
-
|
|
170
|
+
// A session that throws returns no trace; the run still records that it started.
|
|
171
|
+
runSession: (options) => {
|
|
172
|
+
ctx.run.participantStarted();
|
|
173
|
+
return ctx.runSession(options);
|
|
174
|
+
},
|
|
171
175
|
// A local-agent brain runs each participant on the operator's signed-in agent, as on computer
|
|
172
176
|
// use.
|
|
173
177
|
brain: ctx.plan.brain,
|
|
@@ -196,7 +200,7 @@ export function participantRunDeps(ctx, live, scrubKnownValues) {
|
|
|
196
200
|
* scrubbed of the run's known secret values.
|
|
197
201
|
*/
|
|
198
202
|
export async function buildParticipantSpecs(participants, cwd, scrubKnownValues) {
|
|
199
|
-
// Only the personas the participants use: an
|
|
203
|
+
// Only the personas the participants use: an actor.persona that every participant overrides
|
|
200
204
|
// is never applied, so it is not read.
|
|
201
205
|
const personaResolution = await resolveCommittedPersonasForCwd(cwd, participants.map((participant) => participant.personaId));
|
|
202
206
|
const actorSpecs = participants.map((participant) => buildActorSpec(participant, personaResolution.personas));
|
|
@@ -6,12 +6,13 @@ import { DEFAULT_OPENAI_CU_MODEL } from "../../actors/computer-use/openai-provid
|
|
|
6
6
|
import { studyUrlCredentialReason } from "../../study/url-credentials.js";
|
|
7
7
|
import { actorRegistry, isCuaActorDescriptor, } from "../../actors/registry.js";
|
|
8
8
|
import { resolveAutomaticAnalysis } from "../../analysis/automatic-config.js";
|
|
9
|
-
import { brainOf,
|
|
9
|
+
import { brainOf, planCaps, desktopRequirements, isNonEmpty, planBase, } from "../../study/plan-base.js";
|
|
10
10
|
import { sharedWorldParticipants } from "../../study/plan-participants.js";
|
|
11
11
|
import { PUBLIC_TARGET_OWNER_PATTERN } from "../../study/parse/subject.js";
|
|
12
12
|
import { REPO_SLUG_PATTERN } from "../../study/parse/values.js";
|
|
13
|
-
import { concurrentSharedWorldValidationReason, desktopMediaValidationReason, externalPublicSharedWorldValidationReason, outputTokenLimitValidationReason, receivingEmailValidationReason,
|
|
13
|
+
import { concurrentSharedWorldValidationReason, desktopMediaValidationReason, externalPublicSharedWorldValidationReason, outputTokenLimitValidationReason, receivingEmailValidationReason, taskProtocolValidationReason, } from "../../study/validation.js";
|
|
14
14
|
import { MODEL_RATES, unpricedCapMessage } from "../../run/pricing.js";
|
|
15
|
+
import { actorOf, capsOf } from "../../study/study-fields.js";
|
|
15
16
|
/** The registered descriptor for a planned actor; planSharedWorldStudy checked the registry. */
|
|
16
17
|
export function sharedWorldDescriptorOf(actorType) {
|
|
17
18
|
const descriptor = actorRegistry[actorType];
|
|
@@ -38,7 +39,7 @@ export function planSharedWorldStudy(config, input) {
|
|
|
38
39
|
const tasksReason = taskProtocolValidationReason(config, false);
|
|
39
40
|
if (tasksReason)
|
|
40
41
|
return refuse("HUMANISH_STUDY_TASKS_UNSUPPORTED", tasksReason);
|
|
41
|
-
const actorType = config
|
|
42
|
+
const actorType = actorOf(config)?.type ?? "";
|
|
42
43
|
const descriptor = actorRegistry[actorType];
|
|
43
44
|
if (!descriptor || !isCuaActorDescriptor(descriptor))
|
|
44
45
|
return refuse("HUMANISH_SHARED_WORLD_ACTOR_UNSUPPORTED", `actor.type "${actorType}" is not a registered computer-use actor.`);
|
|
@@ -53,17 +54,16 @@ export function planSharedWorldStudy(config, input) {
|
|
|
53
54
|
// A library caller's config skips the parser, which refuses these first.
|
|
54
55
|
studyUrlCredentialReason(config) ??
|
|
55
56
|
outputTokenLimitValidationReason(config) ??
|
|
56
|
-
scenarioCapsValidationReason(config) ??
|
|
57
57
|
(externalPublic
|
|
58
58
|
? externalPublicSharedWorldValidationReason(config)
|
|
59
59
|
: concurrentSharedWorldValidationReason(config));
|
|
60
60
|
if (invalidReason)
|
|
61
61
|
return refuse(invalid, invalidReason, actor);
|
|
62
|
-
if (config
|
|
62
|
+
if (actorOf(config)?.maxOutputTokens !== undefined && input.hasRunSession === true)
|
|
63
63
|
return refuse(invalid, "maxOutputTokens cannot be enforced by a custom runSession.", actor);
|
|
64
|
-
const caps = config
|
|
64
|
+
const caps = capsOf(config);
|
|
65
65
|
if (!input.dryRun && (caps?.maxUsd !== undefined || caps?.maxTotalUsd !== undefined)) {
|
|
66
|
-
const model = (config
|
|
66
|
+
const model = (actorOf(config)?.model ?? DEFAULT_OPENAI_CU_MODEL).trim().toLowerCase();
|
|
67
67
|
if (!MODEL_RATES[model])
|
|
68
68
|
return refuse("HUMANISH_SHARED_WORLD_UNPRICED_CAP", unpricedCapMessage(model), actor);
|
|
69
69
|
}
|
|
@@ -106,7 +106,7 @@ export function planSharedWorldStudy(config, input) {
|
|
|
106
106
|
? {}
|
|
107
107
|
: { sessionTimeoutMs: config.execution.timeoutMs }),
|
|
108
108
|
brain,
|
|
109
|
-
caps:
|
|
109
|
+
caps: planCaps(config),
|
|
110
110
|
requirements: base.dryRun
|
|
111
111
|
? []
|
|
112
112
|
: [
|
|
@@ -48,7 +48,7 @@ import { prepareConcurrentRun } from "./setup.js";
|
|
|
48
48
|
import { browserRouteScorer } from "../../study/adapter-scorer-loader.js";
|
|
49
49
|
import { withLateScorer } from "../../study/route-inputs.js";
|
|
50
50
|
import {} from "./types.js";
|
|
51
|
-
import {
|
|
51
|
+
import { actorOf, participantList } from "../../study/study-fields.js";
|
|
52
52
|
/** The shared-world code for each local-agent refusal, kind for kind with computer-use. */
|
|
53
53
|
const LOCAL_AGENT_REFUSAL_CODES = {
|
|
54
54
|
"agent-missing": "HUMANISH_SHARED_WORLD_AGENT_MISSING",
|
|
@@ -59,11 +59,11 @@ const LOCAL_AGENT_REFUSAL_CODES = {
|
|
|
59
59
|
/** A refused shared-world study's result: the route's envelope, and a refusal's analysis record. */
|
|
60
60
|
export function sharedWorldStudyRefusal(options, refusal) {
|
|
61
61
|
const { config, dryRun } = options;
|
|
62
|
-
const declared =
|
|
62
|
+
const declared = participantList(config) ?? [];
|
|
63
63
|
const fail = concurrentStudyFailure({
|
|
64
64
|
cwd: path.resolve(options.cwd),
|
|
65
65
|
studyId: config.id,
|
|
66
|
-
actor: config
|
|
66
|
+
actor: actorOf(config)?.type ?? "",
|
|
67
67
|
participantCount: declared.length,
|
|
68
68
|
// The parser fills concurrency for multi-participant studies, so this fallback serves only
|
|
69
69
|
// library callers: every declared participant runs at once unless the author declared a cap.
|
|
@@ -54,7 +54,7 @@ export interface SharedWorldRunInput {
|
|
|
54
54
|
* core-stamped onto the bundle as evidence. Absent for library callers. */
|
|
55
55
|
scorerProvenance?: RunScorerProvenance;
|
|
56
56
|
}
|
|
57
|
-
export type ConcurrentSharedWorldStudyErrorCode = "HUMANISH_STUDY_ANALYSIS_INVALID" | "HUMANISH_STUDY_TASKS_UNSUPPORTED" | "HUMANISH_STUDY_OPTION_UNSUPPORTED" | "HUMANISH_STUDY_V2_UNSUPPORTED" | "HUMANISH_SHARED_WORLD_FAILED" | "HUMANISH_SHARED_WORLD_ACTOR_UNSUPPORTED" | "HUMANISH_SHARED_WORLD_INVALID" | "HUMANISH_SHARED_WORLD_KEYS_MISSING"
|
|
57
|
+
export type ConcurrentSharedWorldStudyErrorCode = "HUMANISH_STUDY_ANALYSIS_INVALID" | "HUMANISH_STUDY_TASKS_UNSUPPORTED" | "HUMANISH_STUDY_OPTION_UNSUPPORTED" | "HUMANISH_STUDY_V2_UNSUPPORTED" | "HUMANISH_STUDY_INVALID" | "HUMANISH_SHARED_WORLD_FAILED" | "HUMANISH_SHARED_WORLD_ACTOR_UNSUPPORTED" | "HUMANISH_SHARED_WORLD_INVALID" | "HUMANISH_SHARED_WORLD_KEYS_MISSING"
|
|
58
58
|
/** A local-agent brain's CLI is not on `PATH`. */
|
|
59
59
|
| "HUMANISH_SHARED_WORLD_AGENT_MISSING"
|
|
60
60
|
/** A local-agent brain's CLI is signed out, or could not report its sign-in status. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ActorPersonaRef, ActorTrace } from "../../actors/contract.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { StudyCaps, StudyRuntimeAuth } from "../../study/types.js";
|
|
3
3
|
import { type RunBundle, type RunCostSummary, type BundleRun } from "../../run/bundle.js";
|
|
4
4
|
import { type Verdict } from "../../run/judge.js";
|
|
5
5
|
import type { TerminalLedgers } from "./types.js";
|
|
@@ -21,7 +21,7 @@ export declare function buildTerminalProductBundle(args: {
|
|
|
21
21
|
persona: ActorPersonaRef;
|
|
22
22
|
productName: string;
|
|
23
23
|
publicSurfaces: string[];
|
|
24
|
-
caps?:
|
|
24
|
+
caps?: StudyCaps;
|
|
25
25
|
runtimeAuth?: string;
|
|
26
26
|
stdin: "disabled" | "planned" | "sent";
|
|
27
27
|
policies: {
|
|
@@ -51,7 +51,7 @@ export declare function buildLiveTerminalProductBundle(args: {
|
|
|
51
51
|
persona: ActorPersonaRef;
|
|
52
52
|
productName: string;
|
|
53
53
|
publicSurfaces: string[];
|
|
54
|
-
caps?:
|
|
54
|
+
caps?: StudyCaps;
|
|
55
55
|
runtimeAuthKeyName: string;
|
|
56
56
|
runtimeAuth?: StudyRuntimeAuth;
|
|
57
57
|
policies: {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ActorTokenUsage } from "../../actors/contract.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { StudyCaps } from "../../study/types.js";
|
|
3
3
|
import { type CostCategory } from "../../run/terminal-contract.js";
|
|
4
4
|
import type { CostLine, NoSpendProof, TerminalCostLedger } from "./types.js";
|
|
5
5
|
/**
|
|
@@ -41,7 +41,7 @@ export declare function buildNoSpendProof(ledger: TerminalCostLedger, maxUsd: nu
|
|
|
41
41
|
* grant a green pass: the no-spend proof reports them as unmeasured. maxMinutes is wall-clock and is
|
|
42
42
|
* enforced separately (runWithWallClock); it is not a ledger-derived cap.
|
|
43
43
|
*/
|
|
44
|
-
export declare function evaluateCapsAgainstLedger(ledger: TerminalCostLedger, caps:
|
|
44
|
+
export declare function evaluateCapsAgainstLedger(ledger: TerminalCostLedger, caps: StudyCaps): {
|
|
45
45
|
ok: true;
|
|
46
46
|
} | {
|
|
47
47
|
ok: false;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// How long a live terminal sandbox may live. Its server-side timeout has to cover everything that
|
|
2
|
-
// runs in it before the codex command, the command's own wall clock (
|
|
2
|
+
// runs in it before the codex command, the command's own wall clock (caps.maxMinutes) and
|
|
3
3
|
// the reclamation buffer after that clock's kill. A timeout shorter than that lets E2B kill the
|
|
4
4
|
// sandbox before the command's deadline and before the evidence is read.
|
|
5
5
|
import { NODE_BOOTSTRAP_TIMEOUT_MS } from "../../subject/node-bootstrap.js";
|
|
@@ -117,7 +117,7 @@ export async function finishLiveTerminalSession(inputs) {
|
|
|
117
117
|
const judgment = judgeTerminal({ dryRun: false, participant });
|
|
118
118
|
// The run cost summary, as the computer-use route records it: the sandbox's compute time from
|
|
119
119
|
// its span and observed size, and the participant's tokens (unpriced for Codex). It is not part
|
|
120
|
-
// of the cap ledger above, whose lines sum against
|
|
120
|
+
// of the cap ledger above, whose lines sum against caps.maxUsd.
|
|
121
121
|
const desktops = session.runCostDesktops();
|
|
122
122
|
const runCost = buildRunCostSummary({
|
|
123
123
|
participants: [{ trace }],
|
|
@@ -13,6 +13,7 @@ import { NODE_BOOTSTRAP_TIMEOUT_MS } from "../../subject/node-bootstrap.js";
|
|
|
13
13
|
import { terminalSandboxTimeoutMs } from "./lifetime.js";
|
|
14
14
|
import { isExactRuntimeVersion, TERMINAL_RUNTIME_VERSION_TIMEOUT_MS } from "./runtime.js";
|
|
15
15
|
import { PRODUCT_SETUP_TIMEOUT_MS, TERMINAL_SANDBOX_TIMEOUT_BUFFER_MS, } from "./types.js";
|
|
16
|
+
import { actorOf, capsOf } from "../../study/study-fields.js";
|
|
16
17
|
/**
|
|
17
18
|
* Plan a terminal-product study. It is called for any config handed to the terminal runner, not
|
|
18
19
|
* only one routeOf sends here, so a config for another route gets this route's refusal.
|
|
@@ -40,12 +41,12 @@ export function planTerminalStudy(config, input) {
|
|
|
40
41
|
const tasksReason = taskProtocolValidationReason(config, false);
|
|
41
42
|
if (tasksReason)
|
|
42
43
|
return refuse("HUMANISH_STUDY_TASKS_UNSUPPORTED", tasksReason);
|
|
43
|
-
const actorType = config
|
|
44
|
+
const actorType = actorOf(config)?.type ?? "";
|
|
44
45
|
const descriptor = actorRegistry[actorType];
|
|
45
46
|
if (!descriptor || !isTerminalActorDescriptor(descriptor))
|
|
46
47
|
return refuse("HUMANISH_TERMINAL_ACTOR_UNSUPPORTED", `actor.type "${actorType}" is not a registered terminal actor.`);
|
|
47
48
|
const runtimeVersion = config.execution?.runtime?.version;
|
|
48
|
-
const actor = config
|
|
49
|
+
const actor = actorOf(config);
|
|
49
50
|
if ((config.execution?.runtime !== undefined && !isExactRuntimeVersion(runtimeVersion)) ||
|
|
50
51
|
(actor?.model !== undefined &&
|
|
51
52
|
(typeof actor.model !== "string" || actor.model.trim().length === 0)) ||
|
|
@@ -83,7 +84,7 @@ export function planTerminalStudy(config, input) {
|
|
|
83
84
|
...(egressAllow === undefined ? {} : { egressAllow }),
|
|
84
85
|
...(stdin === undefined ? {} : { stdin }),
|
|
85
86
|
};
|
|
86
|
-
const caps = config
|
|
87
|
+
const caps = capsOf(config);
|
|
87
88
|
if (input.dryRun)
|
|
88
89
|
return { ok: true, plan: { ...shared, dryRun: true, ...(caps ? { caps } : {}) } };
|
|
89
90
|
// The live route injects the runtime key into one command only, and only a registered actor
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// spending. Opt-in openai-env injects the raw key command-scoped, never through
|
|
18
18
|
// Sandbox.create({envs}).
|
|
19
19
|
// Enforced in credentials.ts and runtime-auth.ts.
|
|
20
|
-
// 2. Fail-closed cap. The live key is never exercised without
|
|
20
|
+
// 2. Fail-closed cap. The live key is never exercised without `caps` in force: maxUsd
|
|
21
21
|
// (default/require 0 = no-spend) + maxMinutes (wall-clock kill of the codex command).
|
|
22
22
|
// Enforced in plan.ts (caps required), live-sandbox.ts and lifetime.ts (the wall clock).
|
|
23
23
|
// 3. Public surfaces only. The mission references only subject.product.publicSurfaces + the
|
|
@@ -54,6 +54,7 @@ import { terminalRouteScorer } from "../../study/adapter-scorer-loader.js";
|
|
|
54
54
|
import { withLateScorer } from "../../study/route-inputs.js";
|
|
55
55
|
import {} from "./types.js";
|
|
56
56
|
import { studyResultIdentity } from "../../run/study-result.js";
|
|
57
|
+
import { actorOf } from "../../study/study-fields.js";
|
|
57
58
|
/** A refused terminal study's result: the route's envelope, and the analysis record a refusal gets. */
|
|
58
59
|
export function terminalStudyRefusal(options, refusal) {
|
|
59
60
|
const { config, dryRun } = options;
|
|
@@ -61,7 +62,7 @@ export function terminalStudyRefusal(options, refusal) {
|
|
|
61
62
|
...studyResultIdentity("terminal", config.id),
|
|
62
63
|
ok: false,
|
|
63
64
|
cwd: path.resolve(options.cwd),
|
|
64
|
-
actor: refusal.actor ?? config
|
|
65
|
+
actor: refusal.actor ?? actorOf(config)?.type ?? "",
|
|
65
66
|
product: config.subject.product?.name ?? "",
|
|
66
67
|
dryRun,
|
|
67
68
|
runId: options.runId ?? "not-created",
|
|
@@ -171,7 +171,7 @@ export interface TerminalProductStudyResult extends AutomaticAnalysisResult, Stu
|
|
|
171
171
|
observer?: ObserverResult;
|
|
172
172
|
warnings: string[];
|
|
173
173
|
error?: {
|
|
174
|
-
code: "HUMANISH_STUDY_ANALYSIS_INVALID" | "HUMANISH_STUDY_TASKS_UNSUPPORTED" | "HUMANISH_STUDY_OPTION_UNSUPPORTED" | "HUMANISH_STUDY_V2_UNSUPPORTED" | "HUMANISH_TERMINAL_FAILED" | "HUMANISH_TERMINAL_ACTOR_UNSUPPORTED" | "HUMANISH_TERMINAL_SUBJECT_INVALID" | "HUMANISH_TERMINAL_KEYPLACEMENT_INVALID" | "HUMANISH_TERMINAL_RUNTIME_AUTH_MISSING" | "HUMANISH_TERMINAL_KEYS_MISSING" | "HUMANISH_TERMINAL_CAPS_MISSING" | "HUMANISH_TERMINAL_CAPS_INVALID" | "HUMANISH_TERMINAL_UNPRICED_CAP" | "HUMANISH_TERMINAL_CAPS_EXCEEDED" | "HUMANISH_TERMINAL_CREDENTIAL_DENIED" | "HUMANISH_TERMINAL_CLEANUP_UNPROVEN" | "HUMANISH_RUN_ID_IN_USE";
|
|
174
|
+
code: "HUMANISH_STUDY_ANALYSIS_INVALID" | "HUMANISH_STUDY_TASKS_UNSUPPORTED" | "HUMANISH_STUDY_OPTION_UNSUPPORTED" | "HUMANISH_STUDY_V2_UNSUPPORTED" | "HUMANISH_STUDY_INVALID" | "HUMANISH_TERMINAL_FAILED" | "HUMANISH_TERMINAL_ACTOR_UNSUPPORTED" | "HUMANISH_TERMINAL_SUBJECT_INVALID" | "HUMANISH_TERMINAL_KEYPLACEMENT_INVALID" | "HUMANISH_TERMINAL_RUNTIME_AUTH_MISSING" | "HUMANISH_TERMINAL_KEYS_MISSING" | "HUMANISH_TERMINAL_CAPS_MISSING" | "HUMANISH_TERMINAL_CAPS_INVALID" | "HUMANISH_TERMINAL_UNPRICED_CAP" | "HUMANISH_TERMINAL_CAPS_EXCEEDED" | "HUMANISH_TERMINAL_CREDENTIAL_DENIED" | "HUMANISH_TERMINAL_CLEANUP_UNPROVEN" | "HUMANISH_RUN_ID_IN_USE";
|
|
175
175
|
message: string;
|
|
176
176
|
};
|
|
177
177
|
}
|
package/dist/run/display.d.ts
CHANGED
|
@@ -57,3 +57,13 @@ export declare function bundleDisplayFacts(bundle: DisplayedBundle, status?: unk
|
|
|
57
57
|
* execution failed never reads as a pass beside its participants' verdict.
|
|
58
58
|
*/
|
|
59
59
|
export declare function reviewOutcome(bundle: DisplayedBundle): string;
|
|
60
|
+
/**
|
|
61
|
+
* The summary and gaps a review shows beside its run's display. A run stopped while a participant
|
|
62
|
+
* was still running keeps the review its last live flush wrote, which describes that session as
|
|
63
|
+
* running. Once runDisplay reads the run as interrupted, the review says how many participants the
|
|
64
|
+
* stop cut off instead.
|
|
65
|
+
*/
|
|
66
|
+
export declare function displayedReview<T extends {
|
|
67
|
+
summary: string;
|
|
68
|
+
gaps: string[];
|
|
69
|
+
}>(review: T, bundle: DisplayedBundle, display: RunDisplay): T;
|
package/dist/run/display.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// same facts, so a run whose own ok is false shows as a pass on none of them, and two surfaces
|
|
4
4
|
// cannot disagree about one run.
|
|
5
5
|
import { classifyRunStatus, isRunStatusRecord } from "./status.js";
|
|
6
|
+
import { plural } from "./text.js";
|
|
6
7
|
const LABELS = {
|
|
7
8
|
running: "running",
|
|
8
9
|
interrupted: "interrupted",
|
|
@@ -104,3 +105,24 @@ export function bundleDisplayFacts(bundle, status, nowMs = Date.now()) {
|
|
|
104
105
|
export function reviewOutcome(bundle) {
|
|
105
106
|
return runDisplayLine(runDisplay(bundleDisplayFacts(bundle)));
|
|
106
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* The summary and gaps a review shows beside its run's display. A run stopped while a participant
|
|
110
|
+
* was still running keeps the review its last live flush wrote, which describes that session as
|
|
111
|
+
* running. Once runDisplay reads the run as interrupted, the review says how many participants the
|
|
112
|
+
* stop cut off instead.
|
|
113
|
+
*/
|
|
114
|
+
export function displayedReview(review, bundle, display) {
|
|
115
|
+
const records = bundle.simulations ?? [];
|
|
116
|
+
const running = records.filter((record) => record?.status === "running").length;
|
|
117
|
+
if (display.state !== "interrupted" || running === 0)
|
|
118
|
+
return review;
|
|
119
|
+
const signal = bundle.outcome?.state === "interrupted" ? ` by ${bundle.outcome.signal}` : "";
|
|
120
|
+
const one = running === 1;
|
|
121
|
+
return {
|
|
122
|
+
...review,
|
|
123
|
+
summary: `Interrupted${signal} while ${running} of ${plural(records.length, "participant")} ${one ? "was" : "were"} still running.`,
|
|
124
|
+
gaps: [
|
|
125
|
+
`${plural(running, "participant")} ${one ? "has" : "have"} no verdict: the run stopped before ${one ? "its session" : "their sessions"} ended.`,
|
|
126
|
+
],
|
|
127
|
+
};
|
|
128
|
+
}
|
package/dist/run/reclaim.d.ts
CHANGED
|
@@ -20,9 +20,10 @@ export interface ReclaimResult {
|
|
|
20
20
|
createsInFlight: number;
|
|
21
21
|
/**
|
|
22
22
|
* Why there was nothing to look for: `dry-run` when run.json or status.json records the run as
|
|
23
|
-
* a dry run, which creates no sandboxes
|
|
23
|
+
* a dry run, which creates no sandboxes, and `no-sandbox` when status.json records that the
|
|
24
|
+
* route created none for this run. Reclaim then contacts E2B for nothing.
|
|
24
25
|
*/
|
|
25
|
-
reason?:
|
|
26
|
+
reason?: NoSandboxReason;
|
|
26
27
|
warnings: string[];
|
|
27
28
|
error?: {
|
|
28
29
|
code: "HUMANISH_RECLAIM_RUN_NOT_FOUND" | "HUMANISH_RECLAIM_E2B_DEBUG" | "HUMANISH_RECLAIM_MODULE_UNAVAILABLE" | "HUMANISH_RECLAIM_RECEIPTS_UNREADABLE";
|
|
@@ -47,6 +48,7 @@ export declare function reclaimRunSandboxes(cwd: string, runInput: string, hooks
|
|
|
47
48
|
* also an alias (a run named `latest`) cannot reach another run's receipts.
|
|
48
49
|
*/
|
|
49
50
|
export declare function reclaimPinnedRunSandboxes(cwd: string, runPaths: PreparedRunArtifactPaths, hooks?: ReclaimHooks): Promise<ReclaimResult>;
|
|
51
|
+
type NoSandboxReason = "dry-run" | "no-sandbox";
|
|
50
52
|
/**
|
|
51
53
|
* Kill the sandboxes left by interrupted `humanish study check` probes. A probe journals under
|
|
52
54
|
* .humanish/preflight/<probe-id>/ and removes the journal after a confirmed kill, so what is left
|
package/dist/run/reclaim.js
CHANGED
|
@@ -15,6 +15,7 @@ import { readRunJsonIfExists, resolveRunPath } from "./locate.js";
|
|
|
15
15
|
import { RUN_BUNDLE_FILE } from "./bundle.js";
|
|
16
16
|
import { RUN_STATUS_FILE } from "./status.js";
|
|
17
17
|
import { isRecord } from "./type-guards.js";
|
|
18
|
+
import { isRunOutcome } from "./bundle-shape.js";
|
|
18
19
|
import { parseSandboxOwners, parseSandboxReceipts, SANDBOX_RECEIPTS_ARTIFACT, } from "./sandbox-receipts.js";
|
|
19
20
|
import { sandboxOwnerTags, } from "./sandbox-creates.js";
|
|
20
21
|
import { earlierOutcomes, GONE, RECLAIM_RECEIPT_ARTIFACT, RECLAIM_RESULT_SCHEMA, receiptWriter, rootState, searchTagSets, settledWithin, targetSet, } from "./reclaim-outcomes.js";
|
|
@@ -64,8 +65,9 @@ async function reclaimRun(cwd, runInput, locate, hooks) {
|
|
|
64
65
|
}
|
|
65
66
|
const runId = runIdOf(runPaths);
|
|
66
67
|
// The signal handler passes the creates it stopped and decides from what they reported.
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
const reason = hooks.creates === undefined ? await recordedNoSandbox(runPaths) : undefined;
|
|
69
|
+
if (reason !== undefined)
|
|
70
|
+
return { ...base, ok: true, state: "clean", runId, reason };
|
|
69
71
|
const reclaimed = await reclaimRoot(runPaths, runId, hooks, warnings);
|
|
70
72
|
if (reclaimed.kind === "module-unavailable")
|
|
71
73
|
return {
|
|
@@ -93,29 +95,70 @@ async function reclaimRun(cwd, runInput, locate, hooks) {
|
|
|
93
95
|
};
|
|
94
96
|
}
|
|
95
97
|
/**
|
|
96
|
-
*
|
|
97
|
-
* `dry-run`.
|
|
98
|
-
*
|
|
99
|
-
*
|
|
98
|
+
* Why the run's own records say it created no sandbox. `dry-run`: run.json and status.json, each
|
|
99
|
+
* one present, say `dry-run`. `no-sandbox`: status.json, written at start, names this run and
|
|
100
|
+
* records `sandboxes: none`, which the route sets only when it has no way to create one (a
|
|
101
|
+
* scripted run against an app-url subject), and run.json agrees (scriptedAppUrlBundle). A status
|
|
102
|
+
* record alone can be stale or copied from another run, so it never decides on its own, and an
|
|
103
|
+
* empty journal alone never makes a run clean. A journal with any line in it, or an earlier
|
|
104
|
+
* reclaim receipt that names a sandbox, means a create ran, so that run is reclaimed as a live one
|
|
105
|
+
* whatever its records say.
|
|
100
106
|
*/
|
|
101
|
-
async function
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
async function recordedNoSandbox(runPaths) {
|
|
108
|
+
const bundle = await readRunJsonIfExists(runPaths, RUN_BUNDLE_FILE);
|
|
109
|
+
const status = await readRunJsonIfExists(runPaths, RUN_STATUS_FILE);
|
|
110
|
+
const records = [bundle, status].filter(isRecord);
|
|
111
|
+
const reason = records.length > 0 && records.every((record) => record.mode === "dry-run")
|
|
112
|
+
? "dry-run"
|
|
113
|
+
: isRecord(status) &&
|
|
114
|
+
status.runId === runIdOf(runPaths) &&
|
|
115
|
+
status.sandboxes === "none" &&
|
|
116
|
+
scriptedAppUrlBundle(bundle, runIdOf(runPaths))
|
|
117
|
+
? "no-sandbox"
|
|
118
|
+
: undefined;
|
|
119
|
+
if (reason === undefined)
|
|
120
|
+
return undefined;
|
|
110
121
|
if (!(await containedPathAbsent(runPaths, RECLAIM_RECEIPT_ARTIFACT))) {
|
|
111
122
|
const earlier = await readRunJsonIfExists(runPaths, RECLAIM_RECEIPT_ARTIFACT);
|
|
112
123
|
if (!isRecord(earlier) || !Array.isArray(earlier.outcomes) || earlier.outcomes.length > 0)
|
|
113
|
-
return
|
|
124
|
+
return undefined;
|
|
114
125
|
}
|
|
115
126
|
if (await containedPathAbsent(runPaths, SANDBOX_RECEIPTS_ARTIFACT))
|
|
116
|
-
return
|
|
127
|
+
return reason;
|
|
117
128
|
const journal = await readContainedRegularFile(runPaths, SANDBOX_RECEIPTS_ARTIFACT);
|
|
118
|
-
return journal !== null && journal.toString("utf8").trim() === "";
|
|
129
|
+
return journal !== null && journal.toString("utf8").trim() === "" ? reason : undefined;
|
|
130
|
+
}
|
|
131
|
+
/** The execution failure kinds that leave a sandbox or a provider's resources unconfirmed. */
|
|
132
|
+
const CLEANUP_KINDS = new Set(["sandbox-cleanup", "provider-cleanup"]);
|
|
133
|
+
/**
|
|
134
|
+
* run.json for `runId` agrees that its run could create no sandbox: the scripted route wrote it
|
|
135
|
+
* (each lifecycle event is a `scripted-lab.` one), its subject is not a provisioned clone (a clone
|
|
136
|
+
* records `subject`), it lists no provider resource and no desktop minutes, and its outcome marks
|
|
137
|
+
* no sandbox or provider cleanup unconfirmed. A run.json that is missing, unreadable or says
|
|
138
|
+
* otherwise does not agree.
|
|
139
|
+
*/
|
|
140
|
+
function scriptedAppUrlBundle(bundle, runId) {
|
|
141
|
+
if (!isRecord(bundle) || bundle.runId !== runId || bundle.subject !== undefined)
|
|
142
|
+
return false;
|
|
143
|
+
const { lifecycle, providerResources, cost, outcome } = bundle;
|
|
144
|
+
const scripted = Array.isArray(lifecycle) &&
|
|
145
|
+
lifecycle.length > 0 &&
|
|
146
|
+
lifecycle.every((entry) => isRecord(entry) &&
|
|
147
|
+
typeof entry.event === "string" &&
|
|
148
|
+
entry.event.startsWith("scripted-lab."));
|
|
149
|
+
if (!scripted)
|
|
150
|
+
return false;
|
|
151
|
+
if (providerResources !== undefined &&
|
|
152
|
+
!(Array.isArray(providerResources) && providerResources.length === 0))
|
|
153
|
+
return false;
|
|
154
|
+
if (cost !== undefined && !(isRecord(cost) && cost.desktopMinutes === null))
|
|
155
|
+
return false;
|
|
156
|
+
if (!isRunOutcome(outcome))
|
|
157
|
+
return false;
|
|
158
|
+
if (outcome.state !== "finished")
|
|
159
|
+
return true;
|
|
160
|
+
const { failures, warnings = [] } = outcome.execution;
|
|
161
|
+
return ![...failures, ...warnings].some((failure) => CLEANUP_KINDS.has(failure.kind));
|
|
119
162
|
}
|
|
120
163
|
/**
|
|
121
164
|
* Kill the sandboxes left by interrupted `humanish study check` probes. A probe journals under
|
package/dist/run/results.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ export interface RunResult {
|
|
|
30
30
|
observer?: ObserverResult;
|
|
31
31
|
warnings: string[];
|
|
32
32
|
error?: {
|
|
33
|
-
code: "HUMANISH_STUDY_ANALYSIS_INVALID" | "HUMANISH_STUDY_ANALYSIS_UNSUPPORTED" | "HUMANISH_STUDY_TASKS_UNSUPPORTED" | "HUMANISH_STUDY_OPTION_UNSUPPORTED" | "HUMANISH_STUDY_V2_UNSUPPORTED" | "HUMANISH_STUDY_COMMS_UNSUPPORTED" | "HUMANISH_LIVE_RUN_UNIMPLEMENTED" | "HUMANISH_INVALID_CWD" | "HUMANISH_RUN_ID_IN_USE" | "HUMANISH_INVALID_PARTICIPANT_COUNT" | "HUMANISH_INVALID_PORT" | "HUMANISH_UNSUPPORTED_RERUN_FLAGS" | "HUMANISH_WATCH_OPTION_CONFLICT" | "HUMANISH_WATCH_SAFE_NOT_APPLICABLE" | "HUMANISH_RUN_OPTION_CONFLICT" | "HUMANISH_OBSERVE_OPTION_CONFLICT" | "HUMANISH_STUDY_SCORER_BAD_REF" | "HUMANISH_STUDY_SCORER_NOT_FOUND" | "HUMANISH_STUDY_SCORER_LOAD_FAILED" | "HUMANISH_STUDY_SCORER_NO_HOOKS" | "HUMANISH_STUDY_SCORER_UNSUPPORTED_BACKEND";
|
|
33
|
+
code: "HUMANISH_STUDY_ANALYSIS_INVALID" | "HUMANISH_STUDY_ANALYSIS_UNSUPPORTED" | "HUMANISH_STUDY_TASKS_UNSUPPORTED" | "HUMANISH_STUDY_OPTION_UNSUPPORTED" | "HUMANISH_STUDY_V2_UNSUPPORTED" | "HUMANISH_STUDY_INVALID" | "HUMANISH_STUDY_COMMS_UNSUPPORTED" | "HUMANISH_LIVE_RUN_UNIMPLEMENTED" | "HUMANISH_INVALID_CWD" | "HUMANISH_RUN_ID_IN_USE" | "HUMANISH_INVALID_PARTICIPANT_COUNT" | "HUMANISH_INVALID_PORT" | "HUMANISH_UNSUPPORTED_RERUN_FLAGS" | "HUMANISH_WATCH_OPTION_CONFLICT" | "HUMANISH_WATCH_SAFE_NOT_APPLICABLE" | "HUMANISH_RUN_OPTION_CONFLICT" | "HUMANISH_OBSERVE_OPTION_CONFLICT" | "HUMANISH_STUDY_SCORER_BAD_REF" | "HUMANISH_STUDY_SCORER_NOT_FOUND" | "HUMANISH_STUDY_SCORER_LOAD_FAILED" | "HUMANISH_STUDY_SCORER_NO_HOOKS" | "HUMANISH_STUDY_SCORER_UNSUPPORTED_BACKEND";
|
|
34
34
|
message: string;
|
|
35
35
|
};
|
|
36
36
|
}
|
package/dist/run/run.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ interface StartRunOptions {
|
|
|
11
11
|
mintRunId: () => string;
|
|
12
12
|
mode: "dry-run" | "live";
|
|
13
13
|
study?: RunStudyProvenance | undefined;
|
|
14
|
+
/** `none` when the route creates no sandbox for this run; status.json records it for reclaim. */
|
|
15
|
+
sandboxes?: "none" | undefined;
|
|
14
16
|
/** review.md for the published bundle. */
|
|
15
17
|
renderReview: (bundle: RunBundle) => string;
|
|
16
18
|
/** Used by `FinishedRun.renderObserver`; `render` is the `StudyDeps.renderObserver` seam. */
|
|
@@ -52,6 +54,12 @@ interface Run {
|
|
|
52
54
|
* can never overwrite the final bundle. Routes that throttle snapshots own their timers.
|
|
53
55
|
*/
|
|
54
56
|
writeSnapshot(bundle: RunBundle): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* A participant's session started: its actor began to act on the subject. FinishedRun's
|
|
59
|
+
* participantsRan reads this, so a session that throws before it returns a trace still counts,
|
|
60
|
+
* and a run that failed before any session (a refused E2B key) does not.
|
|
61
|
+
*/
|
|
62
|
+
participantStarted(): void;
|
|
55
63
|
/**
|
|
56
64
|
* The one final publication: run.json with `outcome` set from `outcome`, then the status
|
|
57
65
|
* outcome copied from it, then review.json, review.md, events.ndjson,
|
|
@@ -92,12 +100,14 @@ export declare class FinishedRun {
|
|
|
92
100
|
readonly runId: string;
|
|
93
101
|
/** The paths created by startRun and validated at the start of `finish`. */
|
|
94
102
|
readonly paths: PreparedRunArtifactPaths;
|
|
95
|
-
constructor(key: typeof issueKey, runId: string, paths: PreparedRunArtifactPaths, observer: ObserverTarget | undefined, outcome: RecordedOutcome, recordFailure: (failure: ExecutionFailure) => Promise<RecordedOutcome>, interrupted: () => boolean);
|
|
103
|
+
constructor(key: typeof issueKey, runId: string, paths: PreparedRunArtifactPaths, observer: ObserverTarget | undefined, outcome: RecordedOutcome, recordFailure: (failure: ExecutionFailure) => Promise<RecordedOutcome>, interrupted: () => boolean, participantsRan: () => boolean);
|
|
96
104
|
/**
|
|
97
105
|
* A signal stopped this run's process: status.json and run.json record it `interrupted`. Read
|
|
98
106
|
* when asked, since the signal can arrive after the bundle was published.
|
|
99
107
|
*/
|
|
100
108
|
get interrupted(): boolean;
|
|
109
|
+
/** The route reported a participant's session started (Run.participantStarted). */
|
|
110
|
+
get participantsRan(): boolean;
|
|
101
111
|
static isIssued(value: unknown): value is FinishedRun;
|
|
102
112
|
/**
|
|
103
113
|
* The run's ok and execution outcome as run.json records them: what the route passed to
|