humanish 0.34.0 → 0.35.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/AGENTS.md +8 -0
- package/README.md +11 -7
- package/dist/concurrent-shared-world-lab.d.ts +3 -0
- package/dist/concurrent-shared-world-lab.js +33 -5
- package/dist/concurrent-shared-world-lab.js.map +1 -1
- package/dist/cua-actor-lab.d.ts +11 -3
- package/dist/cua-actor-lab.js +38 -17
- package/dist/cua-actor-lab.js.map +1 -1
- package/dist/init-templates.js +1 -1
- package/dist/lab-config.d.ts +18 -6
- package/dist/lab-config.js +84 -2
- package/dist/lab-config.js.map +1 -1
- package/docs/contracts/schemas.md +37 -15
- package/docs/goals/current.md +1 -1
- package/docs/principles/invariants-and-defaults.md +1 -1
- package/docs/ramp/README.md +5 -4
- package/package.json +1 -1
- package/skills/humanish/SKILL.md +53 -9
package/dist/cua-actor-lab.js
CHANGED
|
@@ -50,8 +50,6 @@ export const CUA_ACTOR_LAB_SCHEMA = "humanish.cua-lab-result.v2";
|
|
|
50
50
|
// The only fan-out topology this slice ships: N lanes = N independent E2B desktop sandboxes,
|
|
51
51
|
// each its own world (clone/serve + subject.state per lane). Shared-world is layer 7 (#164).
|
|
52
52
|
export const CUA_FANOUT_STRATEGY = "per-lane-worlds";
|
|
53
|
-
// Default in-flight lane bound when the config does not declare execution.concurrency.
|
|
54
|
-
const DEFAULT_CUA_CONCURRENCY = 3;
|
|
55
53
|
// Env override that may only LOWER the effective concurrency (never raise concurrent paid
|
|
56
54
|
// desktops — invariant 3). Read names-only into a local; the value never persists.
|
|
57
55
|
const CUA_MAX_CONCURRENCY_ENV = "HUMANISH_CUA_MAX_CONCURRENCY";
|
|
@@ -115,17 +113,30 @@ export function composeLaneInstructions(args) {
|
|
|
115
113
|
* runtime loopback/getHost address (not secret), so — mirroring the lobby-code runtime injection — this
|
|
116
114
|
* augments only the instructions the model receives; the authored prompt + its digest are unchanged.
|
|
117
115
|
* Returns a new spec (never mutates). Shared by the CUA + concurrent shared-world routes. */
|
|
118
|
-
export function withInboxMission(spec, inboxUrl) {
|
|
116
|
+
export function withInboxMission(spec, inboxUrl, address) {
|
|
117
|
+
// The address is half the handoff (#351): the drain matches captured mail against the DECLARED
|
|
118
|
+
// address, so an actor that invents its own at signup gets an inbox that stays empty forever.
|
|
119
|
+
// Telling it which address to use is what makes the funnel deterministic end to end. The
|
|
120
|
+
// wait-steering sentence exists because a mid-flow model treats "we emailed you" as a blocker
|
|
121
|
+
// and ends its session — the exact give-up class a live run documented — unless told the wait
|
|
122
|
+
// is expected and the inbox is the next step.
|
|
123
|
+
const identity = address === undefined ? "" : ` Your email address is ${address} — when the app asks for an email address, enter exactly that.`;
|
|
119
124
|
return {
|
|
120
125
|
...spec,
|
|
121
|
-
instructions: `${spec.instructions}\n\nEmail inbox
|
|
126
|
+
instructions: `${spec.instructions}\n\nEmail inbox:${identity} When the app tells you it has emailed you (a verification link, confirmation code, or magic link), open ${inboxUrl} in the browser to read that email and follow its link or enter its code. All email the app sends you arrives there. Waiting for an email is normal, not a blocker — do not end your session while waiting; open the inbox and refresh it until the email appears.`
|
|
122
127
|
};
|
|
123
128
|
}
|
|
129
|
+
/** The lane's addressed comms recipient, when one exists — the gate AND the address source for the
|
|
130
|
+
* inbox instruction (#351). A lane told to check an inbox it can never receive into would stall,
|
|
131
|
+
* so no addressed recipient means no instruction. */
|
|
132
|
+
export function inboxRecipientFor(commsEmail, laneId) {
|
|
133
|
+
return (commsEmail.recipients ?? []).find((recipient) => recipient.lane === laneId && recipient.address !== undefined);
|
|
134
|
+
}
|
|
124
135
|
/** True when a lane has a declared comms recipient WITH an address, so the drain can actually match the
|
|
125
136
|
* mail the persona will be told to read. Gates the inbox instruction to lanes that can receive mail —
|
|
126
137
|
* a lane told to check an inbox it can never receive into would just stall. */
|
|
127
138
|
export function laneHasInboxRecipient(commsEmail, laneId) {
|
|
128
|
-
return (commsEmail
|
|
139
|
+
return inboxRecipientFor(commsEmail, laneId) !== undefined;
|
|
129
140
|
}
|
|
130
141
|
/** Mid-run inbox-surface render cadence (ms). Coarse enough that the per-tick `cat` + file writes stay
|
|
131
142
|
* cheap; fine enough that a verification email is visible seconds after the app sends it. */
|
|
@@ -195,20 +206,22 @@ function resolvePerLaneSandboxMs(config) {
|
|
|
195
206
|
?? timeoutMs + (provisionedRoute ? SUBJECT_PROVISION_BUDGET_MS + stateBudgetMs : 0) + SANDBOX_TIMEOUT_BUFFER_MS;
|
|
196
207
|
}
|
|
197
208
|
/**
|
|
198
|
-
* Effective in-flight lane bound.
|
|
199
|
-
*
|
|
200
|
-
*
|
|
209
|
+
* Effective in-flight lane bound. Defaults to laneCount — every declared seat runs at once,
|
|
210
|
+
* because a throttle nobody asked for silently turns "N actors live" into waves (#350); total
|
|
211
|
+
* session count and spend are the same either way, only wall-clock and simultaneity differ. A
|
|
212
|
+
* declared execution.concurrency is a CAP, clamped to [1, laneCount]; the env override may only
|
|
213
|
+
* LOWER it (never raise concurrent paid desktops — invariant 3), and a lowering is reported via
|
|
214
|
+
* envLoweredFrom so the plan never silently disagrees with the manifest. Pure given
|
|
215
|
+
* (config, laneCount, env).
|
|
201
216
|
*/
|
|
202
217
|
function resolveCuaConcurrency(config, laneCount, env) {
|
|
203
218
|
const declared = config.execution?.concurrency;
|
|
204
|
-
const base = declared !== undefined
|
|
205
|
-
? Math.min(Math.max(1, declared), laneCount)
|
|
206
|
-
: Math.min(laneCount, DEFAULT_CUA_CONCURRENCY);
|
|
219
|
+
const base = Math.max(1, declared !== undefined ? Math.min(Math.max(1, declared), laneCount) : laneCount);
|
|
207
220
|
const envLower = readPositiveInt(env[CUA_MAX_CONCURRENCY_ENV], 0);
|
|
208
|
-
if (envLower > 0) {
|
|
209
|
-
return Math.max(1, Math.min(base, envLower, laneCount));
|
|
221
|
+
if (envLower > 0 && envLower < base) {
|
|
222
|
+
return { bound: Math.max(1, Math.min(base, envLower, laneCount)), envLoweredFrom: base };
|
|
210
223
|
}
|
|
211
|
-
return
|
|
224
|
+
return { bound: base };
|
|
212
225
|
}
|
|
213
226
|
/** Build the lane specs AND the public plan from a config (pure). countOverride is the CLI
|
|
214
227
|
* --count for homogeneous fan-out (ignored when a `lanes` roster is declared). */
|
|
@@ -250,13 +263,15 @@ function laneSpecsAndPlan(config, opts = {}) {
|
|
|
250
263
|
traceArtifactPath: laneCount === 1 ? "actor.json" : `actors/${streamId}.json`
|
|
251
264
|
});
|
|
252
265
|
}
|
|
253
|
-
const
|
|
266
|
+
const resolved = resolveCuaConcurrency(config, laneCount, env);
|
|
267
|
+
const concurrency = resolved.bound;
|
|
254
268
|
const perLaneSessionBudgetMs = config.execution?.timeoutMs ?? DEFAULT_SESSION_TIMEOUT_MS;
|
|
255
269
|
const perLaneSandboxMs = resolvePerLaneSandboxMs(config);
|
|
256
270
|
const plan = {
|
|
257
271
|
strategy: CUA_FANOUT_STRATEGY,
|
|
258
272
|
laneCount,
|
|
259
273
|
concurrency,
|
|
274
|
+
...(resolved.envLoweredFrom === undefined ? {} : { envLoweredConcurrencyFrom: resolved.envLoweredFrom }),
|
|
260
275
|
waves: Math.ceil(laneCount / concurrency),
|
|
261
276
|
perLaneSessionBudgetMs,
|
|
262
277
|
worstCaseSandboxMinutes: Math.round((laneCount * perLaneSandboxMs) / 60_000),
|
|
@@ -390,7 +405,7 @@ export function resolveCuaLanePlan(config, opts = {}) {
|
|
|
390
405
|
* digests, and budgets only — no prompt text, no secrets). */
|
|
391
406
|
function emitPreflightPlan(plan, labId) {
|
|
392
407
|
const lines = [];
|
|
393
|
-
lines.push(`humanish cua fan-out plan (${labId}): ${plan.laneCount} lane(s), strategy ${plan.strategy}, concurrency ${plan.concurrency}, ${plan.waves} wave(s).`);
|
|
408
|
+
lines.push(`humanish cua fan-out plan (${labId}): ${plan.laneCount} lane(s), strategy ${plan.strategy}, concurrency ${plan.concurrency}${plan.envLoweredConcurrencyFrom === undefined ? "" : ` (lowered from ${plan.envLoweredConcurrencyFrom} by ${CUA_MAX_CONCURRENCY_ENV})`}, ${plan.waves} wave(s).`);
|
|
394
409
|
lines.push(` per-lane session budget ${Math.round(plan.perLaneSessionBudgetMs / 1000)}s; worst-case ~${plan.worstCaseSandboxMinutes} sandbox-minutes total${plan.dryRun ? " (dry-run: $0)" : ""}.`);
|
|
395
410
|
for (const lane of plan.lanes) {
|
|
396
411
|
lines.push(` - ${formatLanePlanEntry(lane)}`);
|
|
@@ -1318,7 +1333,7 @@ export async function runCuaLane(spec, deps) {
|
|
|
1318
1333
|
// Tell the persona where its inbox is — but only when comms is live AND this lane has a declared
|
|
1319
1334
|
// recipient it can actually receive mail into (else it would stall on an inbox that stays empty).
|
|
1320
1335
|
instructions: commsEmail && commsInboxUrl && deployedComms?.ready && laneHasInboxRecipient(commsEmail, spec.laneId)
|
|
1321
|
-
? withInboxMission(spec, commsInboxUrl).instructions
|
|
1336
|
+
? withInboxMission(spec, commsInboxUrl, inboxRecipientFor(commsEmail, spec.laneId)?.address).instructions
|
|
1322
1337
|
: spec.instructions,
|
|
1323
1338
|
persona: spec.persona,
|
|
1324
1339
|
timeoutMs: deps.timeoutMs,
|
|
@@ -1436,6 +1451,12 @@ export async function runCuaLane(spec, deps) {
|
|
|
1436
1451
|
// the address the app actually sends to (e.g. the one the persona surface will sign up with).
|
|
1437
1452
|
warnings.push(`Comms catch captured ${collected.captured} email send(s) but none matched a declared recipient inbox — no comms evidence written. Declare comms.email.recipients[].address to match the address the app sends to.`);
|
|
1438
1453
|
}
|
|
1454
|
+
else {
|
|
1455
|
+
// Zero captures is the silent-broken shape (#351): the app never posted to the catch at
|
|
1456
|
+
// all, so the personas stared at an empty inbox. Most common cause: the app does not
|
|
1457
|
+
// actually read the declared injectEnv var for its email API base URL.
|
|
1458
|
+
warnings.push(`Comms catch captured ZERO email sends — the app never delivered mail through the catch. Verify the app reads ${commsEmail.injectEnv} for its email API base URL (an SDK that ignores it sends real mail or throws) and that the flow reached an email step.`);
|
|
1459
|
+
}
|
|
1439
1460
|
}
|
|
1440
1461
|
catch (error) {
|
|
1441
1462
|
warnings.push(`Comms evidence collection failed (run continues; sandbox still torn down): ${redactText(deps.scrubKnownValues(toErrorMessage(error)))}`);
|