humanish 0.80.0 → 0.81.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.
Files changed (45) hide show
  1. package/README.md +9 -3
  2. package/dist/actor-contract.d.ts +16 -0
  3. package/dist/actor-contract.js.map +1 -1
  4. package/dist/chrome-cdp-probe.js +13 -0
  5. package/dist/chrome-cdp-probe.js.map +1 -1
  6. package/dist/computer-use.d.ts +7 -1
  7. package/dist/computer-use.js +156 -13
  8. package/dist/computer-use.js.map +1 -1
  9. package/dist/cua-actor-lab.d.ts +7 -6
  10. package/dist/cua-actor-lab.js +95 -21
  11. package/dist/cua-actor-lab.js.map +1 -1
  12. package/dist/e2b-desktop-launch.d.ts +28 -1
  13. package/dist/e2b-desktop-launch.js +88 -1
  14. package/dist/e2b-desktop-launch.js.map +1 -1
  15. package/dist/e2b-desktop-screenshot-cleanup.d.ts +15 -0
  16. package/dist/e2b-desktop-screenshot-cleanup.js +67 -0
  17. package/dist/e2b-desktop-screenshot-cleanup.js.map +1 -0
  18. package/dist/e2b-terminal-lab.d.ts +3 -2
  19. package/dist/e2b-terminal-lab.js +176 -85
  20. package/dist/e2b-terminal-lab.js.map +1 -1
  21. package/dist/first-run-path.js +2 -2
  22. package/dist/first-run-path.js.map +1 -1
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/lab-config.d.ts +11 -14
  26. package/dist/lab-config.js +2 -2
  27. package/dist/lab-config.js.map +1 -1
  28. package/dist/openai-responses-cu.js +76 -49
  29. package/dist/openai-responses-cu.js.map +1 -1
  30. package/dist/program.js +16 -7
  31. package/dist/program.js.map +1 -1
  32. package/dist/run.d.ts +1 -1
  33. package/dist/terminal-node-bootstrap.d.ts +4 -0
  34. package/dist/terminal-node-bootstrap.js +58 -0
  35. package/dist/terminal-node-bootstrap.js.map +1 -0
  36. package/dist/terminal-runtime-auth.d.ts +13 -0
  37. package/dist/terminal-runtime-auth.js +24 -0
  38. package/dist/terminal-runtime-auth.js.map +1 -0
  39. package/docs/architecture/actor-contract.md +22 -0
  40. package/docs/architecture/terminal-product-lane.md +108 -7
  41. package/docs/contracts/feedback.md +14 -0
  42. package/docs/contracts/schemas.md +1 -1
  43. package/docs/goals/current.md +45 -13
  44. package/docs/ramp/README.md +1 -1
  45. package/package.json +4 -2
@@ -793,10 +793,11 @@ export declare function captureDesktopBrowserGeometry(args: {
793
793
  */
794
794
  export declare function resolveSelfReportedBlocker(session: CuaLoopResult | undefined): string | undefined;
795
795
  /**
796
- * The friction read of the same narrative (#453): everything the verdict scan counts PLUS
797
- * resolved arcs — a participant who hit a wall, got past it, and said so has reported friction
798
- * worth a tally count and a feedback candidate, without costing the lane its pass. Same
799
- * quoted-copy and stopWhen discipline as the verdict resolver. Exported for testing.
796
+ * Friction is independent of how a completed session ended (#657). Read the participant's
797
+ * redacted messages, including earlier reports, rather than the harness-owned reason that
798
+ * stopWhen/dwell writes. Reasoning, observations, and notices are not participant reports.
799
+ * Resolved arcs still count (#453); quoted copy and negated reports still do not. This read
800
+ * never changes the verdict. Exported for testing.
800
801
  */
801
802
  export declare function resolveSelfReportedFriction(session: CuaLoopResult | undefined): string | undefined;
802
803
  /**
@@ -939,8 +940,8 @@ export declare function buildCuaCostSummary(args: {
939
940
  * trying. A clean pass files nothing here — feedback exists to carry findings, and a run without
940
941
  * any falls back to an honest live summary in the draft layer instead of a template.
941
942
  *
942
- * Everything quoted is already scrub+redacted — `session.reason` passes through redactNarration in
943
- * the loop before it ever lands on a trace — and passes redactText again here as defense-in-depth.
943
+ * Everything quoted is already scrub+redacted — participant messages and `session.reason` pass
944
+ * through redactNarration in the loop — and passes redactText again here as defense-in-depth.
944
945
  */
945
946
  export declare function participantFeedbackCandidates(args: {
946
947
  runId: string;
@@ -1063,7 +1063,11 @@ export async function applyMobileEmulation(desktop, requestTimeoutMs, endpoint,
1063
1063
  throw new Error(`mobile emulation could not be applied (${announced.unavailable}); applied before failing: ${(announced.applied ?? []).join(", ") || "nothing"}`);
1064
1064
  }
1065
1065
  const applied = announced;
1066
- const warnings = [];
1066
+ // Viewport/touch read-back proves context settings, not gesture equivalence. Two hosted
1067
+ // replicas and a native-X conversion-toggle control reproduced reset click counts (#676).
1068
+ const warnings = request.touch
1069
+ ? ["Mobile emulation uses desktop pointer-to-touch conversion, which can differ for repeated taps. Confirm gesture failures with direct or native touch input before attributing them to the app."]
1070
+ : [];
1067
1071
  // The reload inside the applier takes a moment; the read-back is retried until the page reports
1068
1072
  // the requested viewport and user agent, so a slow page does not read as "no proof".
1069
1073
  let readBack = await read();
@@ -1335,14 +1339,56 @@ const REPORTED_DEFECT_LANGUAGE = /\b(defects?|bugs?|accessibilit(y|ies)|inaccess
1335
1339
  // counted as reported friction and became a feedback candidate whose "actual" was a sentence
1336
1340
  // reporting no problem. Only the report-shaped adjectives are negatable here: "no visible focus",
1337
1341
  // "not keyboard-accessible" and "did nothing" are defects and stay.
1338
- const NEGATED_REPORT_LANGUAGE = /\b(?:nothing|no|not|never|without|none)\s+(?:was\s+|were\s+|felt\s+|seemed\s+|really\s+|particularly\s+|especially\s+|major\s+|real\s+|obvious\s+|noticeable\s+)*(?:confus(?:ed|ing|ion)|unclear|unexpected(?:ly)?|hesitat(?:ed|ion|ions)|surpris(?:ed|ing|es)|defects?|bugs?|overlap(?:ped|ping|s)?|truncat(?:ed|es|ion)|hard to (?:find|tell|see|read|reach))\b/g;
1342
+ const NEGATED_REPORT_ITEM = String.raw `(?:confus(?:ed|ing|ion)|unclear(?:\s+error\s+output)?|unexpected(?:ly)?|hesitat(?:ed|ion|ions)|surpris(?:ed|ing|es)|defects?|bugs?|overlap(?:ped|ping|s)?|truncat(?:ed|es|ion)|hard to (?:find|tell|see|read|reach)|blockers?|blocking issues?|errors?(?:\s+output)?|failures?|problems?|issues?)`;
1343
+ const NEGATED_REPORT_QUALIFIERS = String.raw `(?:(?:really|particularly|especially|major|minor|real|actual|remaining|functional|obvious|noticeable|significant|any|a|an)\s+)*`;
1344
+ const NEGATED_REPORT_MODIFIERS = String.raw `(?:(?:was|were|felt|seemed|really|particularly|especially|major|minor|real|actual|remaining|functional|obvious|noticeable|significant|any|a|an|encounter(?:ed)?|experience(?:d)?|notice(?:d)?|observe(?:d)?|feel|find|found|have|had)\s+)*`;
1345
+ const NEGATED_REPORT_LANGUAGE = new RegExp(String.raw `\b(?:nothing|no|not|never|without|none|(?:did|do|does|was|were|has|have|had)n['’]t)\s+${NEGATED_REPORT_MODIFIERS}${NEGATED_REPORT_ITEM}\b`
1346
+ // Negation scopes over a coordinated report list, not the rest of the sentence. In
1347
+ // particular, leave "but the label was confusing" and "and Save did nothing" intact.
1348
+ + String.raw `(?:\s*,?\s+(?:or|nor|and)\s+${NEGATED_REPORT_QUALIFIERS}${NEGATED_REPORT_ITEM}\b)*`
1349
+ // Keep the predicate inside its negation: "No errors blocked me" reports no blocker.
1350
+ + String.raw `(?:\s+(?:blocked|stopped|prevented)\s+(?:me|us|it)\b)?`, "g");
1339
1351
  function stripNegatedReportLanguage(text) {
1340
- return text.replace(NEGATED_REPORT_LANGUAGE, " ");
1352
+ // "Not without hesitation" reports hesitation; do not let the inner "without" erase it.
1353
+ return text.replace(/\b(?:not|never)\s+without\b/g, "with").replace(NEGATED_REPORT_LANGUAGE, " ");
1341
1354
  }
1342
1355
  function completionReasonContradictsGoal(reason) {
1343
- const text = stripQuotedSpans(stripNegatedReportLanguage(stripNegatedNonBlockerPhrases(reason.toLowerCase())));
1356
+ // Preserve the full negated list before the older blocker-specific rules remove its first
1357
+ // noun ("no issues or hesitation"). Matching reports first also avoids their broad encounter
1358
+ // clause rule swallowing a genuine subsequent observation.
1359
+ const text = stripQuotedSpans(stripNegatedNonBlockerPhrases(stripNegatedReportLanguage(stripCodeExamples(reason).toLowerCase())));
1344
1360
  return hasBlockerLanguage(text) || REPORTED_DEFECT_LANGUAGE.test(text);
1345
1361
  }
1362
+ /** Code/documentation excerpts are quoted material, not participant observations. */
1363
+ function stripCodeExamples(text) {
1364
+ return text
1365
+ // Include an unterminated fence: copied text is not promoted just because its closing fence
1366
+ // was omitted. Match the same marker so backticks inside a tilde fence cannot end it early.
1367
+ .replace(/^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?(?:^[ \t]*\1[ \t]*$|(?![\s\S]))/gm, " ")
1368
+ .replace(/(`+)[^`\n]*\1/g, " ");
1369
+ }
1370
+ /** Interim messages also contain plans and hypotheses. Admit observed-report clauses only;
1371
+ * the established closing-report scan remains separate. This is a conservative text heuristic,
1372
+ * not an assertion that every mention of a defect is evidence that one happened. */
1373
+ function interimMessageReportsFriction(message) {
1374
+ const prose = stripQuotedSpans(stripCodeExamples(message).toLowerCase());
1375
+ const clauses = prose.split(/(?<=[.!?])\s+|[;\n]+|,\s*(?:but|so|however|yet)\s+|\s+so\s+(?=i\b|we\b)/);
1376
+ return clauses.some((clause) => {
1377
+ // A condition, question, intention, or conjecture does not assert an observed result.
1378
+ // Clause splitting above keeps "Save did nothing, so I will try Enter" observable.
1379
+ if (/\?|\b(?:if|unless|whether|maybe|perhaps|suppose|hypothetically|might|may|would|should)\b|\bcould\b(?!\s+not\b)/.test(clause)
1380
+ || /\b(?:i|we)(?:['’]ll|\s+(?:will|plan|intend|want|hope|suspect|wonder))\b|\bgoing to\b|\blet['’]s\b/.test(clause)
1381
+ || /\b(?:task|goal|mission|objective|plan)\s+(?:(?:is|was)\s+)?to\b|^\s*(?:check|test|look|checking|testing)\b/.test(clause))
1382
+ return false;
1383
+ // A topic is not a defect ("the accessibility guide is open", "shows error-handling docs").
1384
+ // Actual friction in those surfaces still qualifies: "the error guide was confusing".
1385
+ const observation = clause
1386
+ .replace(/\baccessibilit(?:y|ies)\b/g, " ")
1387
+ .replace(/\berror[- ]handling\b|\berror\s+(?:documentation|docs?|guides?|reference|examples?)\b/g, " ");
1388
+ const assertsObservation = /\b(?:is|are|was|were|has|had|did|does|shows?|showed|seems?|seemed|looks?|looked|found|noticed|saw|hit|encountered|felt|got|failed|returned|cannot|can['’]?t|unable|could not)\b|\b(?:overlap(?:ped|ping|s)?|truncat(?:ed|es)|cut off|nothing happened|no (?:visible )?focus)\b/.test(observation);
1389
+ return assertsObservation && completionReasonContradictsGoal(observation);
1390
+ });
1391
+ }
1346
1392
  /** The verdict scan (strict): like the friction scan, but resolved-arc segments are stripped
1347
1393
  * first — failure narration the participant itself reports as overcome is friction on the
1348
1394
  * road, not a blocker at the destination (#453). */
@@ -1438,21 +1484,39 @@ export function resolveSelfReportedBlocker(session) {
1438
1484
  : undefined;
1439
1485
  }
1440
1486
  /**
1441
- * The friction read of the same narrative (#453): everything the verdict scan counts PLUS
1442
- * resolved arcs — a participant who hit a wall, got past it, and said so has reported friction
1443
- * worth a tally count and a feedback candidate, without costing the lane its pass. Same
1444
- * quoted-copy and stopWhen discipline as the verdict resolver. Exported for testing.
1487
+ * Friction is independent of how a completed session ended (#657). Read the participant's
1488
+ * redacted messages, including earlier reports, rather than the harness-owned reason that
1489
+ * stopWhen/dwell writes. Reasoning, observations, and notices are not participant reports.
1490
+ * Resolved arcs still count (#453); quoted copy and negated reports still do not. This read
1491
+ * never changes the verdict. Exported for testing.
1445
1492
  */
1446
1493
  export function resolveSelfReportedFriction(session) {
1494
+ if (session?.completionReason !== "goal_satisfied")
1495
+ return undefined;
1447
1496
  // Friction stays a read of the narrative even when the outcome was declared: a participant who
1448
1497
  // reached the goal and described what was hard on the way has reported friction.
1449
- if (session?.trace.declaredOutcome === "blocked" && session.completionReason === "goal_satisfied")
1498
+ if (session.trace.declaredOutcome === "blocked")
1450
1499
  return session.reason;
1451
- return session?.completionReason === "goal_satisfied"
1452
- && completionReasonContradictsGoal(session.reason)
1453
- && !traceHasStopWhenMatch(session)
1454
- ? session.reason
1455
- : undefined;
1500
+ const messages = session.trace.items
1501
+ .filter((item) => item.kind === "message" && item.id !== session.trace.debrief?.messageId)
1502
+ .map((item) => item.text?.trim() ?? "")
1503
+ .filter((text) => text.length > 0);
1504
+ // A custom session may keep its closing report only in reason even when earlier messages exist.
1505
+ // Structured stop/dwell reasons are controller text and never enter this closing-report path.
1506
+ const closingReport = traceHasStopWhenMatch(session) ? undefined : session.reason.trim();
1507
+ // One candidate per participant, with exact repeats removed (the closing report often repeats
1508
+ // a prior turn). Earlier turns require observed-report clauses, not arbitrary defect mentions.
1509
+ const typedReports = session.trace.debrief?.status === "completed"
1510
+ ? session.trace.debrief.report?.frictionReports ?? [] : [];
1511
+ const reports = [...new Set([...typedReports, ...messages.filter((message) => message === closingReport
1512
+ ? completionReasonContradictsGoal(message)
1513
+ : interimMessageReportsFriction(message))])];
1514
+ if (closingReport && completionReasonContradictsGoal(closingReport) && !reports.includes(closingReport)) {
1515
+ reports.push(closingReport);
1516
+ }
1517
+ if (reports.length > 0)
1518
+ return reports.join("\n\n");
1519
+ return undefined;
1456
1520
  }
1457
1521
  /**
1458
1522
  * Run ONE E2B desktop lane end-to-end: create the sandbox (per-lane metadata + the lane's device
@@ -1607,13 +1671,11 @@ export async function runCuaLane(spec, deps) {
1607
1671
  dpi: 96,
1608
1672
  lifecycle: { onTimeout: "kill" }
1609
1673
  }, config.execution?.desktop?.template, {
1610
- // One retry on a transient provider error (a sandbox whose envd was not routable yet, an
1611
- // API reply without a body). The failed attempt may have allocated a sandbox this process
1612
- // never learned the id of; its own timeoutMs is what reclaims it, so the warning says so.
1674
+ // The default loader reclaims an acquired handle before retrying failed desktop startup.
1675
+ // Its error names the cleanup outcome; pre-construction allocation failures remain unowned.
1613
1676
  onRetry: (reason) => {
1614
1677
  const named = redactText(deps.scrubKnownValues(reason));
1615
- warnings.push(`Sandbox create for lane ${spec.laneId} retried once after a transient provider error (${named}); ` +
1616
- `a sandbox the first attempt may have allocated is not known to this run and expires on the provider's ${Math.round(deps.perLaneSandboxMs / 60_000)}-minute timeout.`);
1678
+ warnings.push(`Sandbox create for lane ${spec.laneId} retried once after a transient provider error (${named}).`);
1617
1679
  onSubjectPhase({ at: new Date(deps.now()).toISOString(), type: "cua-lab.sandbox.create.retry", message: `sandbox create retried once (${named})` });
1618
1680
  }
1619
1681
  });
@@ -3849,6 +3911,18 @@ export function buildCuaCostSummary(args) {
3849
3911
  sumInput += usage.input ?? 0;
3850
3912
  sumOutput += usage.output ?? 0;
3851
3913
  }
3914
+ // An attempted closing request can fail after provider work without reporting usage.
3915
+ // Keep the known interaction estimate and make the additional unknown explicit.
3916
+ if (lane.trace.debrief?.usageReported === false) {
3917
+ breakdown.push({
3918
+ kind: "model-tokens",
3919
+ ...(lane.laneId === undefined ? {} : { laneId: lane.laneId }),
3920
+ ...(lane.trace.providerVersion === undefined ? {} : { modelId: lane.trace.providerVersion }),
3921
+ estimatedCostUsd: null,
3922
+ reason: "closing_usage_unreported",
3923
+ ratesAsOf: null
3924
+ });
3925
+ }
3852
3926
  const est = lane.trace.estimatedCost;
3853
3927
  if (!est) {
3854
3928
  continue;
@@ -3929,8 +4003,8 @@ function desktopSpanToMinutes(desktopDurationMs) {
3929
4003
  * trying. A clean pass files nothing here — feedback exists to carry findings, and a run without
3930
4004
  * any falls back to an honest live summary in the draft layer instead of a template.
3931
4005
  *
3932
- * Everything quoted is already scrub+redacted — `session.reason` passes through redactNarration in
3933
- * the loop before it ever lands on a trace — and passes redactText again here as defense-in-depth.
4006
+ * Everything quoted is already scrub+redacted — participant messages and `session.reason` pass
4007
+ * through redactNarration in the loop — and passes redactText again here as defense-in-depth.
3934
4008
  */
3935
4009
  export function participantFeedbackCandidates(args) {
3936
4010
  const candidates = [];