humanish 0.118.0 → 0.120.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 +39 -0
- package/dist/analysis/admission.d.ts +16 -2
- package/dist/analysis/admission.js +35 -6
- package/dist/analysis/analysis-limits.d.ts +32 -0
- package/dist/analysis/analysis-limits.js +41 -3
- package/dist/analysis/automatic-config.d.ts +5 -0
- package/dist/analysis/automatic-config.js +15 -1
- package/dist/analysis/cohorts.d.ts +103 -0
- package/dist/analysis/cohorts.js +80 -0
- package/dist/analysis/evidence.d.ts +5 -10
- package/dist/analysis/evidence.js +37 -34
- package/dist/analysis/execute.d.ts +7 -2
- package/dist/analysis/execute.js +242 -297
- package/dist/analysis/provider.js +8 -1
- package/dist/analysis/responses.d.ts +19 -0
- package/dist/analysis/responses.js +201 -0
- package/dist/analysis/store.js +2 -1
- package/dist/analysis/validation.js +13 -7
- package/dist/cli/commands/analyze.js +8 -3
- package/dist/cli/commands/study-format.js +46 -7
- 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 +2 -2
- 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/io.js +2 -3
- package/dist/observer-app.html +2 -2
- package/dist/routes/computer-use/fanout-bundle.js +2 -1
- package/dist/routes/computer-use/fanout-records.js +4 -0
- package/dist/routes/computer-use/participant-execution.d.ts +6 -4
- package/dist/routes/computer-use/participant-execution.js +42 -14
- package/dist/routes/computer-use/participant-model.js +18 -8
- package/dist/routes/computer-use/participant-runs.js +12 -0
- package/dist/routes/computer-use/plan.js +23 -15
- package/dist/routes/computer-use/rerun-selection.js +14 -3
- package/dist/routes/computer-use/types.d.ts +13 -1
- package/dist/routes/shared-world/bundle-records.js +14 -0
- package/dist/routes/shared-world/bundle.js +2 -1
- package/dist/routes/shared-world/external-public.js +15 -3
- package/dist/routes/shared-world/handoff.d.ts +2 -2
- package/dist/routes/shared-world/plan.d.ts +7 -0
- package/dist/routes/shared-world/plan.js +79 -34
- package/dist/routes/shared-world/provisioned.js +15 -8
- package/dist/routes/shared-world/route.js +5 -2
- package/dist/routes/shared-world/types.d.ts +2 -0
- package/dist/run/bundle-shape.js +10 -1
- package/dist/run/bundle.d.ts +15 -0
- package/dist/run/participant-records.js +1 -0
- package/dist/run/route-shell.js +5 -10
- package/dist/run/run.js +4 -2
- package/dist/study/arrivals.d.ts +114 -0
- package/dist/study/arrivals.js +204 -0
- package/dist/study/doctor.js +2 -15
- package/dist/study/keys.d.ts +1 -0
- package/dist/study/keys.js +2 -0
- package/dist/study/migrate/v2.js +3 -2
- package/dist/study/parse/actors.d.ts +1 -1
- package/dist/study/parse/actors.js +9 -3
- package/dist/study/parse/front.js +29 -1
- package/dist/study/plan-base.d.ts +1 -3
- package/dist/study/plan-base.js +0 -3
- package/dist/study/plan-participants.d.ts +2 -0
- package/dist/study/plan-participants.js +1 -0
- package/dist/study/plan-types.d.ts +8 -3
- package/dist/study/preflight-result.js +2 -2
- package/dist/study/preflight.js +12 -0
- package/dist/study/summary.d.ts +3 -3
- package/dist/study/summary.js +5 -3
- package/dist/study/types.d.ts +14 -0
- package/dist/study/validation.js +5 -0
- package/dist/study/warnings.js +11 -3
- package/docs/contracts/run-bundle.md +1 -1
- package/docs/contracts/schemas.md +19 -1
- package/docs/contracts/study-analysis.md +59 -6
- package/docs/principles/invariants-and-defaults.md +14 -14
- package/docs/product/automatic-analysis.md +7 -1
- package/package.json +1 -1
- package/dist/study/automatic-analysis-plan.d.ts +0 -28
- package/dist/study/automatic-analysis-plan.js +0 -57
|
@@ -14,6 +14,7 @@ import { concurrentSharedWorldValidationReason, desktopMediaValidationReason, ex
|
|
|
14
14
|
import { unpricedCapCheck } from "../../study/requirements.js";
|
|
15
15
|
import { participantsAtOnce, sandboxDeadlineRefusal, sandboxHeadroomMs, } from "../../substrates/e2b/lifetime.js";
|
|
16
16
|
import { boundedConcurrency } from "../computer-use/participant-runs.js";
|
|
17
|
+
import { describeArrivals, planStarts, servedScheduleRefusal, } from "../../study/arrivals.js";
|
|
17
18
|
/** The registered descriptor for a planned actor; planSharedWorldStudy checked the registry. */
|
|
18
19
|
export function sharedWorldDescriptorOf(actorType) {
|
|
19
20
|
const descriptor = actorRegistry[actorType];
|
|
@@ -96,53 +97,31 @@ export function planSharedWorldStudy(config, input) {
|
|
|
96
97
|
const plane = planeOf(config);
|
|
97
98
|
if (plane === undefined)
|
|
98
99
|
throw new Error("shared-world validation admitted a plane it cannot plan");
|
|
99
|
-
// Every participant's desktop and, on the provisioned plane, the app's own sandbox share the E2B
|
|
100
|
-
// plan's concurrent sandboxes. A shared world needs two participants live together.
|
|
101
|
-
const declared = config.execution?.concurrency;
|
|
102
|
-
const n = plane.participants.length;
|
|
103
|
-
const atOnce = participantsAtOnce({
|
|
104
|
-
concurrency: boundedConcurrency(declared, n),
|
|
105
|
-
declared: declared !== undefined,
|
|
106
|
-
participants: n,
|
|
107
|
-
alongside: plane.kind === "provisioned" ? 1 : 0,
|
|
108
|
-
minimum: 2,
|
|
109
|
-
}, input.concurrentSandboxes);
|
|
110
|
-
if (!atOnce.ok)
|
|
111
|
-
return refuse(invalid, atOnce.message, actor);
|
|
112
100
|
const sessionTimeoutMs = config.execution?.timeoutMs ?? defaultSessionTimeoutMs(plane, ceiling.ms);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
: {
|
|
123
|
-
name: "each participant's sandbox",
|
|
124
|
-
sessionMs: sessionTimeoutMs,
|
|
125
|
-
sessionDeclared: config.execution?.timeoutMs !== undefined,
|
|
126
|
-
}, ceiling.ms);
|
|
101
|
+
const starts = startsOf(plane, config.execution?.concurrency, input.concurrentSandboxes, sessionTimeoutMs);
|
|
102
|
+
if (!starts.ok)
|
|
103
|
+
return refuse(invalid, starts.message, actor);
|
|
104
|
+
const { arrivals, warnings } = starts;
|
|
105
|
+
const deadlineReason = deadlineReasonOf(plane, arrivals, {
|
|
106
|
+
sessionDeclared: config.execution?.timeoutMs !== undefined,
|
|
107
|
+
ceilingMs: ceiling.ms,
|
|
108
|
+
});
|
|
127
109
|
if (deadlineReason)
|
|
128
110
|
return refuse(invalid, deadlineReason, actor);
|
|
129
111
|
const brain = brainOf(config, false);
|
|
130
112
|
if (brain.kind === "caller")
|
|
131
113
|
throw new Error("a shared-world participant has no caller brain");
|
|
132
|
-
const base = planBase(config, {
|
|
133
|
-
dryRun: input.dryRun,
|
|
134
|
-
analysis,
|
|
135
|
-
participants: n,
|
|
136
|
-
});
|
|
114
|
+
const base = planBase(config, { dryRun: input.dryRun, analysis });
|
|
137
115
|
return {
|
|
138
116
|
ok: true,
|
|
139
117
|
plan: {
|
|
140
118
|
...base,
|
|
141
|
-
...(
|
|
119
|
+
...(warnings.length === 0 ? {} : { warnings }),
|
|
142
120
|
route: "shared-world",
|
|
143
121
|
actor,
|
|
144
122
|
plane,
|
|
145
|
-
concurrency:
|
|
123
|
+
concurrency: starts.count,
|
|
124
|
+
arrivals,
|
|
146
125
|
sessionTimeoutMs,
|
|
147
126
|
brain,
|
|
148
127
|
caps: planCaps(config),
|
|
@@ -164,6 +143,72 @@ export function planSharedWorldStudy(config, input) {
|
|
|
164
143
|
},
|
|
165
144
|
};
|
|
166
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* How many participants run at once, and when each one starts. Every participant's desktop and, on
|
|
148
|
+
* the provisioned plane, the app's own sandbox share the E2B plan's concurrent sandboxes, and a
|
|
149
|
+
* shared world needs two participants live together. On the external-public plane the host runs
|
|
150
|
+
* on a slot of its own and the others queue for the rest (external-public.ts).
|
|
151
|
+
*/
|
|
152
|
+
function startsOf(plane, declared, limit, sessionMs) {
|
|
153
|
+
const n = plane.participants.length;
|
|
154
|
+
const atOnce = participantsAtOnce({
|
|
155
|
+
concurrency: boundedConcurrency(declared, n),
|
|
156
|
+
declared: declared !== undefined,
|
|
157
|
+
participants: n,
|
|
158
|
+
alongside: plane.kind === "provisioned" ? 1 : 0,
|
|
159
|
+
minimum: 2,
|
|
160
|
+
}, limit);
|
|
161
|
+
if (!atOnce.ok)
|
|
162
|
+
return atOnce;
|
|
163
|
+
const hostIndex = plane.participants.findIndex((participant) => participant.host === true);
|
|
164
|
+
const queue = {
|
|
165
|
+
slots: atOnce.count,
|
|
166
|
+
sessionMs,
|
|
167
|
+
...(plane.kind === "external-public" && hostIndex >= 0 ? { ownSlot: hostIndex } : {}),
|
|
168
|
+
};
|
|
169
|
+
return {
|
|
170
|
+
ok: true,
|
|
171
|
+
count: atOnce.count,
|
|
172
|
+
...planStarts(plane.participants, queue, atOnce.lowered),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Why the run's longest-lived sandbox cannot live as long as the plan needs. On the provisioned
|
|
177
|
+
* plane that is the subject sandbox, which serves the app for one session and then until the last
|
|
178
|
+
* participant ends; on the external-public plane, each participant's own sandbox, created at its
|
|
179
|
+
* start.
|
|
180
|
+
*/
|
|
181
|
+
function deadlineReasonOf(plane, arrivals, options) {
|
|
182
|
+
const { sessionDeclared, ceilingMs } = options;
|
|
183
|
+
const sessionMs = arrivals.sessionMs;
|
|
184
|
+
if (plane.kind !== "provisioned")
|
|
185
|
+
return sandboxDeadlineRefusal({ name: "each participant's sandbox", sessionMs, sessionDeclared }, ceilingMs);
|
|
186
|
+
const seed = plane.subject.state.seed ?? [];
|
|
187
|
+
return (sandboxDeadlineRefusal({
|
|
188
|
+
name: "the subject sandbox, which serves the app until every participant ends",
|
|
189
|
+
sessionMs,
|
|
190
|
+
sessionDeclared,
|
|
191
|
+
servedSubject: { seed },
|
|
192
|
+
}, ceilingMs) ?? servedScheduleRefusal(arrivals, { sessionMs, seed }, ceilingMs));
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* The schedule lines a shared-world run prints before it creates anything, when the study declares
|
|
196
|
+
* `participants[].startAfterMs`: the schedule, and the worst case in sandbox-minutes, which counts
|
|
197
|
+
* every participant's sandbox (its session plus the teardown buffer) and on the provisioned plane
|
|
198
|
+
* the app's sandbox until the last participant ends.
|
|
199
|
+
*/
|
|
200
|
+
export function sharedWorldScheduleLines(plan) {
|
|
201
|
+
const participants = plan.plane.participants.length;
|
|
202
|
+
const participantMs = participants * (plan.sessionTimeoutMs + sandboxHeadroomMs());
|
|
203
|
+
const subjectMs = plan.plane.kind === "provisioned"
|
|
204
|
+
? plan.arrivals.lastEndMs + sandboxHeadroomMs({ seed: plan.plane.subject.state.seed ?? [] })
|
|
205
|
+
: 0;
|
|
206
|
+
return [
|
|
207
|
+
`humanish shared-world plan (${plan.studyId}): ${participants} participants, at most ${plan.concurrency} at once; worst-case ~${Math.round((participantMs + subjectMs) / 60_000)} sandbox-minutes total${plan.dryRun ? " (dry-run: $0)" : ""}.`,
|
|
208
|
+
` schedule: ${describeArrivals(plan.arrivals)}`,
|
|
209
|
+
"",
|
|
210
|
+
].join("\n");
|
|
211
|
+
}
|
|
167
212
|
// The default per-participant session budget is derived from the plane. On a provisioned plane the
|
|
168
213
|
// binding constraint is the subject sandbox (it must outlive every participant: timeoutMs +
|
|
169
214
|
// provisioning + seeding + teardown buffer, under the sandbox ceiling), so the derivation hands each
|
|
@@ -6,7 +6,7 @@ import { buildOriginMap } from "../../comms/capture-surface.js";
|
|
|
6
6
|
import { deployCommsCatch, refreshInboxSurface, writeInboxSurface, } from "../../comms/sandbox-catch.js";
|
|
7
7
|
import { redactText, toErrorMessage } from "../../evidence/redaction.js";
|
|
8
8
|
import { liveObserverResult } from "../../observer/live.js";
|
|
9
|
-
import {
|
|
9
|
+
import { runOnSchedule } from "../../study/arrivals.js";
|
|
10
10
|
import { provisionCloneSubject } from "../../subject/clone.js";
|
|
11
11
|
import { provisionLocalTreeSubject } from "../../subject/local-tree.js";
|
|
12
12
|
import { defaultSharedWorldPhaseSink } from "../../subject/steps.js";
|
|
@@ -96,7 +96,7 @@ class SubjectPlane {
|
|
|
96
96
|
this.setup.stateSnapshots.push({ timestamp, digest: snapshot.digest });
|
|
97
97
|
}
|
|
98
98
|
async acquire() {
|
|
99
|
-
const { plan, deps, env, requestTimeoutMs
|
|
99
|
+
const { plan, deps, env, requestTimeoutMs } = this.ctx;
|
|
100
100
|
const { subjectEnvNames, commsEnv } = this.setup;
|
|
101
101
|
const subjectModule = await (deps.desktopModule ?? loadE2BDesktopModule)();
|
|
102
102
|
// The one subject sandbox: headless service host (no GUI participant). The subject env is
|
|
@@ -107,7 +107,9 @@ class SubjectPlane {
|
|
|
107
107
|
module: subjectModule,
|
|
108
108
|
apiKey: this.ctx.e2bApiKey,
|
|
109
109
|
requestTimeoutMs,
|
|
110
|
-
|
|
110
|
+
// The app serves until the last participant ends, which a late start or a wait for a free
|
|
111
|
+
// slot puts past one session.
|
|
112
|
+
sessionTimeoutMs: plan.arrivals.lastEndMs,
|
|
111
113
|
seed: planeStateOf(plan)?.seed ?? [],
|
|
112
114
|
metadata: {
|
|
113
115
|
...CONCURRENT_SHARED_WORLD_PROVIDER_METADATA,
|
|
@@ -380,15 +382,20 @@ async function publishInProgress(plane, ctx, live, setup) {
|
|
|
380
382
|
}
|
|
381
383
|
startParticipantFlush(ctx, live, inProgressBundle);
|
|
382
384
|
}
|
|
383
|
-
// Launch N actor sandboxes
|
|
384
|
-
// runCuaParticipants would add a pipeline gate and fail-fast). Each actor's window is
|
|
385
|
-
// one orchestrator clock.
|
|
385
|
+
// Launch N actor sandboxes independently on the study's schedule (runCuaParticipant +
|
|
386
|
+
// runOnSchedule; runCuaParticipants would add a pipeline gate and fail-fast). Each actor's window is
|
|
387
|
+
// measured on the one orchestrator clock.
|
|
386
388
|
function runParticipants(plane, ctx, live, setup) {
|
|
387
389
|
const { now } = ctx;
|
|
388
390
|
const participants = ctx.plan.plane.participants;
|
|
389
391
|
const { commsEmail } = setup;
|
|
390
392
|
const baseActorDeps = participantRunDeps(ctx, live, ctx.scrubKnownValues);
|
|
391
|
-
|
|
393
|
+
const schedule = {
|
|
394
|
+
startAfterMs: (spec) => spec.planned.startAfterMs,
|
|
395
|
+
slots: Math.max(1, ctx.concurrency),
|
|
396
|
+
now,
|
|
397
|
+
};
|
|
398
|
+
return runOnSchedule(ctx.actorSpecs, schedule, async (spec, i, { scheduledAt }) => {
|
|
392
399
|
const route = resolveActorEntryUrl(plane.getHostUrl, participants[i]?.entry);
|
|
393
400
|
// Tell this persona its (getHost-reachable) inbox URL, but only when comms is live and this participant
|
|
394
401
|
// has a declared recipient it can actually receive mail into (else it would stall on an empty
|
|
@@ -400,7 +407,7 @@ function runParticipants(plane, ctx, live, setup) {
|
|
|
400
407
|
const startedAt = now();
|
|
401
408
|
const outcome = await runCuaParticipant(actorSpec, { ...baseActorDeps, appUrl: route });
|
|
402
409
|
const endedAt = now();
|
|
403
|
-
return { spec, outcome, startedAt, endedAt, route };
|
|
410
|
+
return { spec, outcome, scheduledAt, startedAt, endedAt, route };
|
|
404
411
|
});
|
|
405
412
|
}
|
|
406
413
|
export async function runProvisionedPlane(ctx, live, setup) {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
// per-delta→actor field, so causation under concurrency is structurally inexpressible.
|
|
24
24
|
//
|
|
25
25
|
// Capability and proof: the deterministic $0 gate proves the plumbing + the claims-match-mechanism
|
|
26
|
-
// contract: the real
|
|
26
|
+
// contract: the real start queue (runOnSchedule) produces genuinely overlapping laneWindows (a rendezvous
|
|
27
27
|
// latch in the fake session forces two participant fns in-flight while the real orchestrator clock
|
|
28
28
|
// measures the windows). Every generated bundle describes only its own observations; no one run
|
|
29
29
|
// establishes scale, repeatability, or adopter-harness replacement.
|
|
@@ -38,7 +38,7 @@ import {} from "../../run/run.js";
|
|
|
38
38
|
import { RunSecrets } from "../../run/secrets.js";
|
|
39
39
|
import { admitRoute, completeRefusalAnalysis, } from "../../run/route-shell.js";
|
|
40
40
|
import { runExternalPublicPlane } from "./external-public.js";
|
|
41
|
-
import { sharedWorldDescriptorOf } from "./plan.js";
|
|
41
|
+
import { sharedWorldDescriptorOf, sharedWorldScheduleLines, } from "./plan.js";
|
|
42
42
|
import { runProvisionedPlane } from "./provisioned.js";
|
|
43
43
|
import { concurrentStudyFailure, finishConcurrentRun } from "./result.js";
|
|
44
44
|
import { prepareConcurrentRun } from "./setup.js";
|
|
@@ -143,6 +143,9 @@ async function runPlanInScope(plan, input, config, scope) {
|
|
|
143
143
|
const deps = input.deps ?? {};
|
|
144
144
|
const env = input.env ?? process.env;
|
|
145
145
|
const fail = sharedWorldFailure(plan, input);
|
|
146
|
+
// Before any sandbox or provider call, as computer use prints its participant plan.
|
|
147
|
+
if (plan.arrivals.declared)
|
|
148
|
+
process.stderr.write(sharedWorldScheduleLines(plan));
|
|
146
149
|
const descriptor = sharedWorldDescriptorOf(plan.actor);
|
|
147
150
|
const planeClass = plan.plane.kind === "external-public" ? "external-public" : "provisioned-getHost";
|
|
148
151
|
// The provisioned plane's subject; the external-public plane has none.
|
|
@@ -138,6 +138,8 @@ export interface ConcurrentSharedWorldStudyResult extends AutomaticAnalysisResul
|
|
|
138
138
|
export interface ActorRunResult {
|
|
139
139
|
spec: DesktopParticipantRun;
|
|
140
140
|
outcome: ParticipantRunOutcome;
|
|
141
|
+
/** When the schedule had it start, in epoch ms; the host of an external public app starts with the run. */
|
|
142
|
+
scheduledAt: number;
|
|
141
143
|
startedAt: number;
|
|
142
144
|
endedAt: number;
|
|
143
145
|
route: string;
|
package/dist/run/bundle-shape.js
CHANGED
|
@@ -313,7 +313,16 @@ function isRunSimulation(value) {
|
|
|
313
313
|
Array.isArray(value.streamIds) &&
|
|
314
314
|
value.streamIds.every((streamId) => typeof streamId === "string") &&
|
|
315
315
|
typeof value.startedAt === "string" &&
|
|
316
|
-
typeof value.updatedAt === "string"
|
|
316
|
+
typeof value.updatedAt === "string" &&
|
|
317
|
+
(value.arrival === undefined || isRunParticipantArrival(value.arrival)));
|
|
318
|
+
}
|
|
319
|
+
function isRunParticipantArrival(value) {
|
|
320
|
+
return (isRecord(value) &&
|
|
321
|
+
typeof value.startAfterMs === "number" &&
|
|
322
|
+
Number.isSafeInteger(value.startAfterMs) &&
|
|
323
|
+
value.startAfterMs >= 0 &&
|
|
324
|
+
(value.scheduledAt === undefined || typeof value.scheduledAt === "string") &&
|
|
325
|
+
(value.startedAt === undefined || typeof value.startedAt === "string"));
|
|
317
326
|
}
|
|
318
327
|
function isRunAdapterArtifact(value) {
|
|
319
328
|
return (isRecord(value) &&
|
package/dist/run/bundle.d.ts
CHANGED
|
@@ -123,6 +123,21 @@ export interface RunSimulation {
|
|
|
123
123
|
streamIds: string[];
|
|
124
124
|
startedAt: string;
|
|
125
125
|
updatedAt: string;
|
|
126
|
+
/**
|
|
127
|
+
* When a computer-use or shared-world participant was due and when it started, in a study that
|
|
128
|
+
* declares `participants[].startAfterMs`. `startAfterMs` is its offset (0 when its entry declares
|
|
129
|
+
* none), `scheduledAt` the moment the run started its participants plus that offset, and
|
|
130
|
+
* `startedAt` when its desktop was requested: later than `scheduledAt` when it waited for a free
|
|
131
|
+
* slot. A dry run records only the offset, and a participant that never started has no
|
|
132
|
+
* `startedAt`. The record's own `startedAt` above is the run's creation time.
|
|
133
|
+
*/
|
|
134
|
+
arrival?: RunParticipantArrival;
|
|
135
|
+
}
|
|
136
|
+
/** A participant record's `arrival`. */
|
|
137
|
+
export interface RunParticipantArrival {
|
|
138
|
+
startAfterMs: number;
|
|
139
|
+
scheduledAt?: string;
|
|
140
|
+
startedAt?: string;
|
|
126
141
|
}
|
|
127
142
|
export interface RunEvent {
|
|
128
143
|
id: string;
|
package/dist/run/route-shell.js
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
// either is made here once. The preview route records no run and has no shell.
|
|
5
5
|
import { completeAutomaticAnalysis } from "../analysis/automatic-completion.js";
|
|
6
6
|
import { resolveAutomaticAnalysis } from "../analysis/automatic-config.js";
|
|
7
|
-
import { withParticipantLimitSkip } from "../study/automatic-analysis-plan.js";
|
|
8
7
|
import { runScope } from "./run.js";
|
|
9
8
|
import { withTransientCommsSecrets } from "./transient-comms-secrets.js";
|
|
10
9
|
/**
|
|
@@ -13,15 +12,11 @@ import { withTransientCommsSecrets } from "./transient-comms-secrets.js";
|
|
|
13
12
|
* it takes closes the run it started, and only a run that published its final bundle is analyzed.
|
|
14
13
|
*/
|
|
15
14
|
export async function admitRoute(shell) {
|
|
16
|
-
const complete = (input, result, finished) =>
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
?
|
|
20
|
-
|
|
21
|
-
...(shell.analysis === undefined ? {} : { trigger: shell.analysis.trigger }),
|
|
22
|
-
preferLargerOutput: shell.analysis?.preferLargerOutput === true,
|
|
23
|
-
...(shell.analysisRefusal === undefined ? {} : { refusal: shell.analysisRefusal }),
|
|
24
|
-
});
|
|
15
|
+
const complete = (input, result, finished) => completeAutomaticAnalysis(result, finished, shell.analysis?.config, input, {
|
|
16
|
+
...(shell.analysis === undefined ? {} : { trigger: shell.analysis.trigger }),
|
|
17
|
+
preferLargerOutput: shell.analysis?.preferLargerOutput === true,
|
|
18
|
+
...(shell.analysisRefusal === undefined ? {} : { refusal: shell.analysisRefusal }),
|
|
19
|
+
});
|
|
25
20
|
const admission = await shell.admit();
|
|
26
21
|
if (!admission.ok)
|
|
27
22
|
return {
|
package/dist/run/run.js
CHANGED
|
@@ -117,13 +117,15 @@ function observerTarget(options) {
|
|
|
117
117
|
function runPublisher(args) {
|
|
118
118
|
const { options, runId, createdAt, paths, runStatus, now, admit, participantsRan } = args;
|
|
119
119
|
const observer = observerTarget(options);
|
|
120
|
-
// The study's warnings join whatever events the route wrote, once, in every write.
|
|
120
|
+
// The study's warnings join whatever events the route wrote, once, in every write. A warning can
|
|
121
|
+
// quote the study file, which may name a sandbox URL a participant needs, so each goes through
|
|
122
|
+
// the redaction a run failure gets.
|
|
121
123
|
const studyWarnings = (options.warnings ?? []).map((message, index) => ({
|
|
122
124
|
id: `event-study-warning-${String(index + 1).padStart(3, "0")}`,
|
|
123
125
|
at: createdAt,
|
|
124
126
|
level: "warn",
|
|
125
127
|
type: "study.warning",
|
|
126
|
-
message,
|
|
128
|
+
message: redactText(message),
|
|
127
129
|
}));
|
|
128
130
|
const withStudyWarnings = (events) => [
|
|
129
131
|
...events.filter((event) => !studyWarnings.some((warning) => warning.id === event.id)),
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { RunParticipantArrival } from "../run/bundle.js";
|
|
2
|
+
/**
|
|
3
|
+
* The latest start a study may declare: 24 hours, the longest sandbox lifetime E2B documents. It
|
|
4
|
+
* also keeps every wait under Node's timer limit of 2^31 - 1 ms, past which a timer fires at once.
|
|
5
|
+
*/
|
|
6
|
+
export declare const LATEST_START_AFTER_MS: number;
|
|
7
|
+
/** Whether `value` is a start offset humanish runs: whole milliseconds from 0 to 24 hours. */
|
|
8
|
+
export declare function isStartAfterMs(value: unknown): value is number;
|
|
9
|
+
/** The refusal of a start offset that is not one, naming its `field`. */
|
|
10
|
+
export declare function startAfterMessage(field: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Why a roster's declared starts cannot run, or undefined. A start orders participants against
|
|
13
|
+
* each other, so it needs two or more of them, and the host of an external public app opens the
|
|
14
|
+
* session the others join, so it starts with the run.
|
|
15
|
+
*/
|
|
16
|
+
export declare function rosterStartsReason(roster: readonly {
|
|
17
|
+
readonly id?: string;
|
|
18
|
+
readonly startAfterMs?: number;
|
|
19
|
+
readonly host?: boolean;
|
|
20
|
+
}[]): string | undefined;
|
|
21
|
+
/** What the plan says about when a run's participants start, with every session at its full budget. */
|
|
22
|
+
export interface ArrivalPlan {
|
|
23
|
+
/** Whether any participant declares `startAfterMs`. */
|
|
24
|
+
readonly declared: boolean;
|
|
25
|
+
/** The earliest and the latest declared start, in ms after the run starts its participants. */
|
|
26
|
+
readonly firstStartMs: number;
|
|
27
|
+
readonly lastStartMs: number;
|
|
28
|
+
/** The most participants running at once. */
|
|
29
|
+
readonly peak: number;
|
|
30
|
+
/** How many participants wait past their time for a free slot, and the longest of those waits. */
|
|
31
|
+
readonly waiting: number;
|
|
32
|
+
readonly longestWaitMs: number;
|
|
33
|
+
/** When the last session ends at the latest, in ms after the run starts its participants. */
|
|
34
|
+
readonly lastEndMs: number;
|
|
35
|
+
/** The session budget every figure above assumes each participant uses. */
|
|
36
|
+
readonly sessionMs: number;
|
|
37
|
+
}
|
|
38
|
+
/** How the runtime starts participants: how many run at once, and for how long at most. */
|
|
39
|
+
export interface StartQueue {
|
|
40
|
+
readonly slots: number;
|
|
41
|
+
readonly sessionMs: number;
|
|
42
|
+
/**
|
|
43
|
+
* The roster index of a participant that runs on a slot of its own, outside the queue: the
|
|
44
|
+
* external-public host, which the others wait on. It counts against `slots`.
|
|
45
|
+
*/
|
|
46
|
+
readonly ownSlot?: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The schedule a roster's declared offsets give under the queue (`undefined` is 0: the participant
|
|
50
|
+
* starts with the run). Participants start in schedule order, by offset and then roster position,
|
|
51
|
+
* each at its time or when a slot frees, whichever is later; this is the rule runOnSchedule
|
|
52
|
+
* follows.
|
|
53
|
+
*/
|
|
54
|
+
export declare function planArrivals(offsets: readonly (number | undefined)[], queue: StartQueue): ArrivalPlan;
|
|
55
|
+
/**
|
|
56
|
+
* The plan's schedule, after a "schedule:" label: the first and last start, and how many run at
|
|
57
|
+
* once at the most.
|
|
58
|
+
*/
|
|
59
|
+
export declare function describeArrivals(arrivals: ArrivalPlan): string;
|
|
60
|
+
/**
|
|
61
|
+
* A planner's arrival plan for its participants under the start queue, and the warnings the plan
|
|
62
|
+
* records about it. A participant's desktop is created when it starts, so on computer use a late
|
|
63
|
+
* start adds nothing to any sandbox's deadline; the provisioned shared world checks its app's
|
|
64
|
+
* sandbox with servedScheduleRefusal. `lowered` is the warning that the E2B plan's limit runs the
|
|
65
|
+
* roster in waves.
|
|
66
|
+
*/
|
|
67
|
+
export declare function planStarts(participants: readonly {
|
|
68
|
+
readonly startAfterMs?: number;
|
|
69
|
+
}[], queue: StartQueue, lowered: string | undefined): {
|
|
70
|
+
arrivals: ArrivalPlan;
|
|
71
|
+
warnings: string[];
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Why a sandbox that serves the app until the last participant ends, the provisioned shared
|
|
75
|
+
* world's subject sandbox, cannot live that long under `ceilingMs`; undefined when it can. Its
|
|
76
|
+
* deadline is the schedule's `lastEndMs` plus the time to provision and seed the subject and tear
|
|
77
|
+
* it down. The session alone is checked first (sandboxDeadlineRefusal), so this names what the
|
|
78
|
+
* schedule adds: a late start or a wait for a free slot.
|
|
79
|
+
*/
|
|
80
|
+
export declare function servedScheduleRefusal(arrivals: ArrivalPlan, sandbox: {
|
|
81
|
+
readonly sessionMs: number;
|
|
82
|
+
readonly seed: readonly {
|
|
83
|
+
readonly timeoutMs?: number | undefined;
|
|
84
|
+
}[];
|
|
85
|
+
}, ceilingMs: number): string | undefined;
|
|
86
|
+
/** When a participant was due and when it started, in epoch ms. One that never started has no `startedAt`. */
|
|
87
|
+
export interface ParticipantArrival {
|
|
88
|
+
readonly scheduledAt: number;
|
|
89
|
+
readonly startedAt?: number;
|
|
90
|
+
}
|
|
91
|
+
/** A participant's `arrival` in run.json: its offset, and on a live run when it was due and started. */
|
|
92
|
+
export declare function arrivalRecord(startAfterMs: number | undefined, arrival: ParticipantArrival | undefined): RunParticipantArrival;
|
|
93
|
+
/** Where a participant sits in the run's schedule when its start comes. */
|
|
94
|
+
export interface ScheduledStart {
|
|
95
|
+
/** Its place in schedule order, from 0: the first participant to start has 0. */
|
|
96
|
+
readonly order: number;
|
|
97
|
+
/** Its time in epoch ms: the moment the run started its participants plus its offset. */
|
|
98
|
+
readonly scheduledAt: number;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Calls `start` for each item at its time, with at most `slots` calls in flight. Items start in
|
|
102
|
+
* schedule order; one whose time comes while every slot is busy waits for the next free slot. An
|
|
103
|
+
* abort of `signal` ends every wait at once, so `start` runs for the waiting items right away and
|
|
104
|
+
* decides whether to skip them. Results come back in the items' order.
|
|
105
|
+
*/
|
|
106
|
+
export declare function runOnSchedule<T, R>(items: readonly T[], options: {
|
|
107
|
+
readonly startAfterMs: (item: T) => number | undefined;
|
|
108
|
+
readonly slots: number;
|
|
109
|
+
/** The run's clock in epoch ms, which must advance while a start waits. Defaults to Date.now. */
|
|
110
|
+
readonly now?: () => number;
|
|
111
|
+
/** Waits `ms` or until `signal` aborts. Defaults to node:timers/promises' setTimeout. */
|
|
112
|
+
readonly sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
113
|
+
readonly signal?: AbortSignal;
|
|
114
|
+
}, start: (item: T, index: number, scheduled: ScheduledStart) => Promise<R>): Promise<R[]>;
|