humanish 0.38.0 → 0.40.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/README.md +5 -2
- package/dist/actor-contract.d.ts +24 -1
- package/dist/actor-contract.js +4 -0
- package/dist/actor-contract.js.map +1 -1
- package/dist/claude-agent-sdk.js +4 -0
- package/dist/claude-agent-sdk.js.map +1 -1
- package/dist/comms-catch-host.d.ts +31 -0
- package/dist/comms-catch-host.js +82 -4
- package/dist/comms-catch-host.js.map +1 -1
- package/dist/comms-sandbox-catch.d.ts +32 -1
- package/dist/comms-sandbox-catch.js +226 -19
- package/dist/comms-sandbox-catch.js.map +1 -1
- package/dist/computer-use-actor.d.ts +1 -1
- package/dist/computer-use.d.ts +13 -1
- package/dist/computer-use.js +100 -14
- package/dist/computer-use.js.map +1 -1
- package/dist/concurrent-shared-world-lab.js +7 -2
- package/dist/concurrent-shared-world-lab.js.map +1 -1
- package/dist/cua-actor-lab.d.ts +8 -0
- package/dist/cua-actor-lab.js +110 -9
- package/dist/cua-actor-lab.js.map +1 -1
- package/dist/e2b-desktop-executor.d.ts +21 -6
- package/dist/e2b-desktop-executor.js +65 -21
- package/dist/e2b-desktop-executor.js.map +1 -1
- package/dist/lab-config.d.ts +36 -0
- package/dist/lab-config.js +87 -2
- package/dist/lab-config.js.map +1 -1
- package/dist/observer-data.js +5 -0
- package/dist/observer-data.js.map +1 -1
- package/dist/openai-responses-cu.js +10 -1
- package/dist/openai-responses-cu.js.map +1 -1
- package/dist/orientation.d.ts +29 -0
- package/dist/orientation.js +95 -0
- package/dist/orientation.js.map +1 -0
- package/dist/persona-resolve.d.ts +44 -0
- package/dist/persona-resolve.js +109 -0
- package/dist/persona-resolve.js.map +1 -0
- package/dist/pi-agent-core.js +4 -0
- package/dist/pi-agent-core.js.map +1 -1
- package/dist/pricing.d.ts +8 -0
- package/dist/pricing.js +22 -6
- package/dist/pricing.js.map +1 -1
- package/dist/program.js +36 -1
- package/dist/program.js.map +1 -1
- package/dist/run.d.ts +1 -1
- package/dist/run.js +27 -1
- package/dist/run.js.map +1 -1
- package/dist/shared-world-lab.js +7 -2
- package/dist/shared-world-lab.js.map +1 -1
- package/dist/subject-runtime.d.ts +23 -0
- package/dist/subject-runtime.js +70 -0
- package/dist/subject-runtime.js.map +1 -0
- package/docs/contracts/schemas.md +1 -1
- package/docs/goals/current.md +2 -2
- package/docs/principles/three-roles.md +68 -0
- package/docs/ramp/README.md +1 -1
- package/package.json +1 -1
package/dist/cua-actor-lab.js
CHANGED
|
@@ -40,6 +40,9 @@ import { mapWithConcurrency } from "./concurrency.js";
|
|
|
40
40
|
import { appendSandboxReceipt } from "./sandbox-receipts.js";
|
|
41
41
|
import { assertScreenshotEvidence } from "./image-evidence.js";
|
|
42
42
|
import { buildObserverData } from "./observer-data.js";
|
|
43
|
+
import { corepackCommandFor, needsNodeRuntime, nodeBootstrapCommand } from "./subject-runtime.js";
|
|
44
|
+
import { personaToDirectives, renderPersonaPromptSection } from "./persona.js";
|
|
45
|
+
import { labPersonaIds, resolveCommittedPersonas } from "./persona-resolve.js";
|
|
43
46
|
import { attachObserverRuntimeStreamUrls, renderObserver } from "./observer.js";
|
|
44
47
|
import { containsSensitive, digestText, redactedTail, redactText } from "./redaction.js";
|
|
45
48
|
import { assertPreparedSelectedOutputDirectory, assertSafeOutputPathSegment, prepareContainedOutputDirectory, prepareSelectedOutputDirectory, writeContainedOutputFile, writePreparedRunLatestPointer } from "./selected-output-paths.js";
|
|
@@ -73,6 +76,10 @@ const BROWSER_SETTLE_MS = 8_000;
|
|
|
73
76
|
const SANDBOX_TIMEOUT_BUFFER_MS = 10 * 60_000;
|
|
74
77
|
// Room the clone route adds to the sandbox deadline for clone/install/build/start/probe.
|
|
75
78
|
const SUBJECT_PROVISION_BUDGET_MS = 30 * 60_000;
|
|
79
|
+
/** E2B refuses a sandbox lifetime over one hour ("400: Timeout cannot be greater than 1 hours").
|
|
80
|
+
* The derived per-lane deadline has to stay under it, and saying so at plan time beats discovering
|
|
81
|
+
* it from a raw provider 400 after a plan has already printed. */
|
|
82
|
+
const MAX_SANDBOX_MS = 60 * 60_000;
|
|
76
83
|
export const SUBJECT_DIR = "/home/user/subject";
|
|
77
84
|
// Remote path for the once-per-run packed local-tree archive; removed by the extract step
|
|
78
85
|
// after it unpacks into SUBJECT_DIR.
|
|
@@ -94,8 +101,15 @@ export function composeLaneInstructions(args) {
|
|
|
94
101
|
const deviceLine = preset.isMobile
|
|
95
102
|
? `You are a mobile user on a ${name} device (${preset.width}x${preset.height} @${preset.deviceScaleFactor}x). Expect a mobile/touch layout.`
|
|
96
103
|
: `You are a desktop user (${name}, ${preset.width}x${preset.height}).`;
|
|
104
|
+
// A resolved persona contributes its compiled directives (friction tolerance, skill bias,
|
|
105
|
+
// accessibility behavior, constraints) through the SAME persona.ts compiler the terminal lane
|
|
106
|
+
// uses, so one persona file means one behavior across every route.
|
|
107
|
+
const personaLine = args.resolvedPersona
|
|
108
|
+
? renderPersonaPromptSection(args.resolvedPersona)
|
|
109
|
+
: args.persona ? `Persona: ${args.persona}.` : undefined;
|
|
110
|
+
const traitsApplied = args.resolvedPersona ? personaToDirectives(args.resolvedPersona).traitsApplied : [];
|
|
97
111
|
const parts = [
|
|
98
|
-
|
|
112
|
+
personaLine,
|
|
99
113
|
deviceLine,
|
|
100
114
|
args.mission,
|
|
101
115
|
args.instruction ? `Lane focus: ${args.instruction}` : undefined
|
|
@@ -105,7 +119,7 @@ export function composeLaneInstructions(args) {
|
|
|
105
119
|
instructions,
|
|
106
120
|
persona: {
|
|
107
121
|
id: args.persona ?? "cua-operator",
|
|
108
|
-
traitsApplied
|
|
122
|
+
traitsApplied,
|
|
109
123
|
promptDigest: digestText(instructions, 16)
|
|
110
124
|
}
|
|
111
125
|
};
|
|
@@ -239,9 +253,12 @@ function laneSpecsAndPlan(config, opts = {}) {
|
|
|
239
253
|
const simId = `sim-${String(i + 1).padStart(3, "0")}`;
|
|
240
254
|
const streamId = `stream-${String(i + 1).padStart(3, "0")}`;
|
|
241
255
|
const device = resolveLaneDevice(config, lane);
|
|
256
|
+
const personaId = (roster ? lane?.persona : actor?.persona);
|
|
257
|
+
const resolvedPersona = personaId === undefined ? undefined : opts.personas?.get(personaId);
|
|
242
258
|
const composed = composeLaneInstructions({
|
|
243
259
|
mission,
|
|
244
|
-
...(
|
|
260
|
+
...(personaId === undefined ? {} : { persona: personaId }),
|
|
261
|
+
...(resolvedPersona === undefined ? {} : { resolvedPersona }),
|
|
245
262
|
...(((roster ? lane?.instruction : actor?.laneFocus?.instruction)) === undefined ? {} : { instruction: (roster ? lane?.instruction : actor?.laneFocus?.instruction) }),
|
|
246
263
|
device: { name: device.name, preset: device.preset }
|
|
247
264
|
});
|
|
@@ -1049,6 +1066,7 @@ export function resolveSelfReportedBlocker(session) {
|
|
|
1049
1066
|
*/
|
|
1050
1067
|
export async function runCuaLane(spec, deps) {
|
|
1051
1068
|
const { config, appUrl, cloneRoute, localTreeRoute, serve, subjectRepo, subjectEnvNames } = deps;
|
|
1069
|
+
const subjectEnvValues = config.subject.envValues ?? {};
|
|
1052
1070
|
const targetUrl = spec.targetUrl ?? appUrl;
|
|
1053
1071
|
const env = deps.env;
|
|
1054
1072
|
// Off-app comms (#297): on an in-sandbox subject route, redirect the app's email-API sends into an
|
|
@@ -1066,6 +1084,18 @@ export async function runCuaLane(spec, deps) {
|
|
|
1066
1084
|
const commsEnv = commsEmail?.injectEnv !== undefined && commsPort !== undefined
|
|
1067
1085
|
? { [commsEmail.injectEnv]: `http://127.0.0.1:${commsPort}` }
|
|
1068
1086
|
: {};
|
|
1087
|
+
// SMTP transport: the same idea as injectEnv, but an app that speaks SMTP needs a host and a port
|
|
1088
|
+
// rather than a base URL. The catch accepts any credentials (loopback only), yet many apps refuse
|
|
1089
|
+
// to boot unless the user/password vars exist at all, so those are injected when declared.
|
|
1090
|
+
const commsSmtpPort = commsEmail?.smtp?.port;
|
|
1091
|
+
if (commsEmail?.smtp && commsSmtpPort !== undefined) {
|
|
1092
|
+
commsEnv[commsEmail.smtp.hostEnv] = "127.0.0.1";
|
|
1093
|
+
commsEnv[commsEmail.smtp.portEnv] = String(commsSmtpPort);
|
|
1094
|
+
if (commsEmail.smtp.userEnv)
|
|
1095
|
+
commsEnv[commsEmail.smtp.userEnv] = commsEmail.smtp.user ?? "humanish";
|
|
1096
|
+
if (commsEmail.smtp.passwordEnv)
|
|
1097
|
+
commsEnv[commsEmail.smtp.passwordEnv] = commsEmail.smtp.password ?? "humanish";
|
|
1098
|
+
}
|
|
1069
1099
|
// Persona inbox SURFACE (#297 slice B): the loopback URL the persona opens to read captured mail; the
|
|
1070
1100
|
// origin-rewrite map (identity on this same-sandbox route, but covers localhost/0.0.0.0 alias skew + an
|
|
1071
1101
|
// operator-declared linkOrigin); and a disposable background loop that renders the surface DURING the
|
|
@@ -1155,8 +1185,17 @@ export async function runCuaLane(spec, deps) {
|
|
|
1155
1185
|
},
|
|
1156
1186
|
// Env placement per the doctrine: the ACTOR's key never enters the sandbox (the model drives
|
|
1157
1187
|
// from outside). The SUBJECT's declared env NAMES are provisioned here on the clone route.
|
|
1158
|
-
|
|
1159
|
-
|
|
1188
|
+
// Three sources, in precedence order: committed non-secret config (subject.envValues), then
|
|
1189
|
+
// secret values forwarded from the caller's environment (subject.env), then the harness's own
|
|
1190
|
+
// comms wiring, which must win because only it knows the catch's address.
|
|
1191
|
+
...(subjectEnvNames.length > 0 || Object.keys(subjectEnvValues).length > 0 || Object.keys(commsEnv).length > 0
|
|
1192
|
+
? {
|
|
1193
|
+
envs: {
|
|
1194
|
+
...subjectEnvValues,
|
|
1195
|
+
...Object.fromEntries(subjectEnvNames.map((name) => [name, env[name]])),
|
|
1196
|
+
...commsEnv
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1160
1199
|
: {}),
|
|
1161
1200
|
resolution: spec.resolution,
|
|
1162
1201
|
dpi: 96,
|
|
@@ -1175,7 +1214,11 @@ export async function runCuaLane(spec, deps) {
|
|
|
1175
1214
|
// into its env at create) resolves the moment it boots. A comms-declared lab that can't stand the
|
|
1176
1215
|
// catch up is a setup failure (fail closed) rather than silently sending real mail.
|
|
1177
1216
|
if (commsEmail && commsPort !== undefined) {
|
|
1178
|
-
deployedComms = await deployCommsCatch(desktop, {
|
|
1217
|
+
deployedComms = await deployCommsCatch(desktop, {
|
|
1218
|
+
port: commsPort,
|
|
1219
|
+
...(commsSmtpPort === undefined ? {} : { smtpPort: commsSmtpPort }),
|
|
1220
|
+
requestTimeoutMs: deps.requestTimeoutMs
|
|
1221
|
+
});
|
|
1179
1222
|
if (!deployedComms.ready) {
|
|
1180
1223
|
throw new Error(`comms email catch did not become ready on 127.0.0.1:${commsPort} in the subject sandbox`);
|
|
1181
1224
|
}
|
|
@@ -1351,7 +1394,7 @@ export async function runCuaLane(spec, deps) {
|
|
|
1351
1394
|
? {}
|
|
1352
1395
|
: {
|
|
1353
1396
|
maxUsd,
|
|
1354
|
-
estimateTurnCostUsd: (input, output) => estimateActorCost({ input, output }, capModelId).estimatedCostUsd
|
|
1397
|
+
estimateTurnCostUsd: (input, output, cachedInput) => estimateActorCost({ input, output, ...(cachedInput === undefined ? {} : { cachedInput }) }, capModelId).estimatedCostUsd
|
|
1355
1398
|
}),
|
|
1356
1399
|
desktop: desktop,
|
|
1357
1400
|
...(launchedBrowserFamily === "chromium"
|
|
@@ -1461,7 +1504,10 @@ export async function runCuaLane(spec, deps) {
|
|
|
1461
1504
|
// Zero captures is the silent-broken shape (#351): the app never posted to the catch at
|
|
1462
1505
|
// all, so the personas stared at an empty inbox. Most common cause: the app does not
|
|
1463
1506
|
// actually read the declared injectEnv var for its email API base URL.
|
|
1464
|
-
|
|
1507
|
+
const transportHint = commsEmail.smtp
|
|
1508
|
+
? `Verify the app reads ${commsEmail.smtp.hostEnv}/${commsEmail.smtp.portEnv} for its SMTP host and port`
|
|
1509
|
+
: `Verify the app reads ${commsEmail.injectEnv} for its email API base URL (an SDK that ignores it sends real mail or throws)`;
|
|
1510
|
+
warnings.push(`Comms catch captured ZERO email sends — the app never delivered mail through the catch. ${transportHint} and that the flow reached an email step.`);
|
|
1465
1511
|
}
|
|
1466
1512
|
}
|
|
1467
1513
|
catch (error) {
|
|
@@ -1965,11 +2011,28 @@ export async function runCuaActorLab(options) {
|
|
|
1965
2011
|
if (fanoutReason) {
|
|
1966
2012
|
return fail("HUMANISH_CUA_LAB_FANOUT_INVALID", fanoutReason, descriptor.id);
|
|
1967
2013
|
}
|
|
2014
|
+
// The sandbox deadline is DERIVED from the session budget, so a lab can ask for a session that
|
|
2015
|
+
// cannot legally be provisioned. Catch it here, before anything is created, and show the
|
|
2016
|
+
// arithmetic — the provider's own error names a limit but not which knob produced it.
|
|
2017
|
+
const derivedSandboxMs = resolvePerLaneSandboxMs(config);
|
|
2018
|
+
if (derivedSandboxMs > MAX_SANDBOX_MS) {
|
|
2019
|
+
const provisionedRoute = config.subject.source === "clone" || config.subject.source === "local-tree";
|
|
2020
|
+
const sessionMs = config.execution?.timeoutMs ?? DEFAULT_SESSION_TIMEOUT_MS;
|
|
2021
|
+
const headroomMs = derivedSandboxMs - sessionMs;
|
|
2022
|
+
return fail("HUMANISH_CUA_LAB_SUBJECT_INVALID", `execution.timeoutMs ${Math.round(sessionMs / 60_000)}m derives a ${Math.round(derivedSandboxMs / 60_000)}m sandbox deadline, and a sandbox may not live longer than ${MAX_SANDBOX_MS / 60_000}m. The deadline is the session budget plus ${Math.round(headroomMs / 60_000)}m of provisioning and teardown headroom${provisionedRoute ? " (this route clones, installs, builds and serves the subject before the actor starts)" : ""}. Lower execution.timeoutMs to at most ${Math.round((MAX_SANDBOX_MS - headroomMs) / 60_000)}m, or set execution.desktop.sandboxTimeoutMs explicitly.`, descriptor.id);
|
|
2023
|
+
}
|
|
2024
|
+
// Compile any committed personas BEFORE planning, so the plan builder stays pure and each lane's
|
|
2025
|
+
// prompt carries real behavioral directives rather than a bare `Persona: <id>.` label (#381).
|
|
2026
|
+
const personaResolution = await resolveCommittedPersonas(projectRoot, labPersonaIds(config));
|
|
2027
|
+
for (const warning of personaResolution.warnings) {
|
|
2028
|
+
process.stderr.write(`humanish: ${warning}\n`);
|
|
2029
|
+
}
|
|
1968
2030
|
// Resolve the lane plan (pure) — the SAME table for dry-run and live.
|
|
1969
2031
|
let { lanes: laneSpecs, plan } = laneSpecsAndPlan(config, {
|
|
1970
2032
|
...(options.countOverride === undefined ? {} : { countOverride: options.countOverride }),
|
|
1971
2033
|
env,
|
|
1972
|
-
dryRun
|
|
2034
|
+
dryRun,
|
|
2035
|
+
personas: personaResolution.personas
|
|
1973
2036
|
});
|
|
1974
2037
|
let laneCount = laneSpecs.length;
|
|
1975
2038
|
if (laneCount > MAX_CUA_LANES) {
|
|
@@ -2555,6 +2618,39 @@ async function runSubjectServePipeline(desktop, args) {
|
|
|
2555
2618
|
}
|
|
2556
2619
|
emitPhaseCompleted(args.onPhase, now, groupStartedAt, `state.${when}`, true, `subject state seed steps complete (${when})`);
|
|
2557
2620
|
};
|
|
2621
|
+
// Provide the runtime the pipeline needs before running it (#371). The stock desktop template
|
|
2622
|
+
// ships python3 and curl but no Node, so an `npm install` here used to die at exit 127 after the
|
|
2623
|
+
// sandbox was already paid for. Probe-first, so a template that ships its own Node pays nothing.
|
|
2624
|
+
const serveCommands = [args.serve.install, args.serve.build, args.serve.start];
|
|
2625
|
+
if (needsNodeRuntime(serveCommands)) {
|
|
2626
|
+
const runtimeStartedAt = now();
|
|
2627
|
+
emitPhaseStarted(args.onPhase, now, "runtime", "providing the Node runtime the serve pipeline needs");
|
|
2628
|
+
const bootstrap = await runDetachedStep(desktop, {
|
|
2629
|
+
name: "subject-runtime-node",
|
|
2630
|
+
command: nodeBootstrapCommand(),
|
|
2631
|
+
cwd: SUBJECT_DIR,
|
|
2632
|
+
timeoutMs: args.serve.installTimeoutMs ?? INSTALL_TIMEOUT_MS,
|
|
2633
|
+
requestTimeoutMs: args.requestTimeoutMs,
|
|
2634
|
+
...timers
|
|
2635
|
+
});
|
|
2636
|
+
let ok = bootstrap.ok;
|
|
2637
|
+
const corepack = ok ? corepackCommandFor(serveCommands) : undefined;
|
|
2638
|
+
if (corepack) {
|
|
2639
|
+
const pm = await runDetachedStep(desktop, {
|
|
2640
|
+
name: "subject-runtime-pm",
|
|
2641
|
+
command: corepack,
|
|
2642
|
+
cwd: SUBJECT_DIR,
|
|
2643
|
+
timeoutMs: args.serve.installTimeoutMs ?? INSTALL_TIMEOUT_MS,
|
|
2644
|
+
requestTimeoutMs: args.requestTimeoutMs,
|
|
2645
|
+
...timers
|
|
2646
|
+
});
|
|
2647
|
+
ok = pm.ok;
|
|
2648
|
+
}
|
|
2649
|
+
emitPhaseCompleted(args.onPhase, now, runtimeStartedAt, "runtime", ok, ok ? "Node runtime ready" : "could not provide a Node runtime");
|
|
2650
|
+
if (!ok) {
|
|
2651
|
+
throw new Error(`the subject's serve pipeline needs a Node runtime and this desktop template has none, and bootstrapping one failed: ${tailOf(args.scrub(bootstrap.logTail))}. Use execution.desktop.template with an image that ships Node, or change serve.install to a runtime the template provides.`);
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2558
2654
|
if (args.serve.install) {
|
|
2559
2655
|
const installStartedAt = now();
|
|
2560
2656
|
emitPhaseStarted(args.onPhase, now, "install", "installing subject dependencies");
|
|
@@ -3661,6 +3757,11 @@ function verdictForStatus(status) {
|
|
|
3661
3757
|
return "blocked";
|
|
3662
3758
|
case "timed_out":
|
|
3663
3759
|
return "timed_out";
|
|
3760
|
+
// A participant who abandoned, or a session that ran out before the goal, did not pass — but the
|
|
3761
|
+
// harness did not fail either. The run reports what happened rather than a verdict on the tool.
|
|
3762
|
+
case "abandoned":
|
|
3763
|
+
case "incomplete":
|
|
3764
|
+
return "fail";
|
|
3664
3765
|
}
|
|
3665
3766
|
}
|
|
3666
3767
|
function renderCuaReviewMarkdown(bundle) {
|