humanish 0.45.0 → 0.47.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/dist/actor-contract.d.ts +10 -0
- package/dist/actor-contract.js.map +1 -1
- package/dist/computer-use-actor.d.ts +3 -1
- package/dist/computer-use-actor.js +2 -1
- package/dist/computer-use-actor.js.map +1 -1
- package/dist/computer-use.d.ts +10 -1
- package/dist/computer-use.js +27 -14
- package/dist/computer-use.js.map +1 -1
- package/dist/concurrent-shared-world-lab.js +37 -7
- package/dist/concurrent-shared-world-lab.js.map +1 -1
- package/dist/cua-actor-lab.d.ts +17 -2
- package/dist/cua-actor-lab.js +146 -6
- package/dist/cua-actor-lab.js.map +1 -1
- package/dist/observer-app.html +6 -6
- package/dist/openai-responses-cu.d.ts +12 -0
- package/dist/openai-responses-cu.js +56 -18
- package/dist/openai-responses-cu.js.map +1 -1
- package/dist/run.d.ts +15 -1
- package/dist/run.js.map +1 -1
- package/docs/contracts/schemas.md +6 -2
- package/docs/goals/current.md +39 -39
- package/docs/ramp/README.md +1 -1
- package/package.json +2 -2
package/dist/cua-actor-lab.js
CHANGED
|
@@ -579,6 +579,7 @@ function blockedLaneOutcome(spec, reason) {
|
|
|
579
579
|
skippedReason: reason,
|
|
580
580
|
noEngagement: false,
|
|
581
581
|
selfReportedBlocker: false,
|
|
582
|
+
reportedFriction: false,
|
|
582
583
|
harnessError: false
|
|
583
584
|
};
|
|
584
585
|
}
|
|
@@ -1073,13 +1074,47 @@ async function startDesktopStream(desktop, browserWindowId) {
|
|
|
1073
1074
|
await desktop.stream.start({ requireAuth: true });
|
|
1074
1075
|
}
|
|
1075
1076
|
}
|
|
1076
|
-
function
|
|
1077
|
-
const text = stripQuotedSpans(stripNegatedNonBlockerPhrases(reason.toLowerCase()));
|
|
1077
|
+
function hasBlockerLanguage(text) {
|
|
1078
1078
|
return /\b(can'?t|cannot|could not|unable|blocked|blocker|failed|invalid|not set)\b/.test(text)
|
|
1079
1079
|
|| /\b(shows|showing|hit|encountered|returned|got)\b.{0,80}\berror\b/.test(text)
|
|
1080
1080
|
|| /\berror[:.]/.test(text)
|
|
1081
1081
|
|| /what would you like me to do|please tell me|need (the )?(task|credentials|instructions)/.test(text);
|
|
1082
1082
|
}
|
|
1083
|
+
/** The friction scan (inclusive): does the narrative report ANY blocker-shaped language,
|
|
1084
|
+
* resolved or not? Feeds the participants `reportedFriction` tally and feedback candidates. */
|
|
1085
|
+
function completionReasonContradictsGoal(reason) {
|
|
1086
|
+
return hasBlockerLanguage(stripQuotedSpans(stripNegatedNonBlockerPhrases(reason.toLowerCase())));
|
|
1087
|
+
}
|
|
1088
|
+
/** The verdict scan (strict): like the friction scan, but resolved-arc segments are stripped
|
|
1089
|
+
* first — failure narration the participant itself reports as overcome is friction on the
|
|
1090
|
+
* road, not a blocker at the destination (#453). */
|
|
1091
|
+
function completionReasonBlocksVerdict(reason) {
|
|
1092
|
+
return hasBlockerLanguage(stripResolvedArcSegments(stripQuotedSpans(stripNegatedNonBlockerPhrases(reason.toLowerCase()))));
|
|
1093
|
+
}
|
|
1094
|
+
// A failure segment counts as a resolved arc when the recovery is self-reported either in the
|
|
1095
|
+
// SAME segment ("the import failed but then went through") or — the common report shape — in the
|
|
1096
|
+
// immediately FOLLOWING segment as a retry/alternative that succeeded ("my first import failed
|
|
1097
|
+
// with a parser error. A simpler SQL import succeeded."). The lookahead demands the retry flavor
|
|
1098
|
+
// on purpose: unrelated praise ("Separately, the search box worked") must never launder an
|
|
1099
|
+
// unresolved failure. "Login failed so I gave up" has no recovery anywhere and stays a blocker.
|
|
1100
|
+
// (#453 — the run-1 false negative: a defect report after demonstrated success failed the lane,
|
|
1101
|
+
// an incentive inversion against exactly the participant behavior a study wants most.)
|
|
1102
|
+
const RESOLUTION_TERMS = /\b(succeed(?:ed|s)?|success(?:ful|fully)?|worked|works around|then worked|now works?|resolved|fixed|recovered|got it working|went through)\b/;
|
|
1103
|
+
const RETRY_RESOLUTION = /\b(simpler|simplified|retry(?:ing)?|retried|second (?:attempt|try)|another (?:attempt|try|approach)|different (?:approach|way|route)|instead|then|eventually|after that)\b[^.!?\n]{0,80}\b(succeed(?:ed|s)?|success(?:ful|fully)?|worked|went through|completed|passed)\b/;
|
|
1104
|
+
/** Drop sentence/bullet segments whose failure language is part of a self-reported RESOLVED arc. */
|
|
1105
|
+
function stripResolvedArcSegments(text) {
|
|
1106
|
+
const segments = text.split(/(?<=[.!?])\s+|\n+/);
|
|
1107
|
+
return segments
|
|
1108
|
+
.filter((segment, index) => {
|
|
1109
|
+
if (!hasBlockerLanguage(segment))
|
|
1110
|
+
return true;
|
|
1111
|
+
if (RESOLUTION_TERMS.test(segment))
|
|
1112
|
+
return false;
|
|
1113
|
+
const next = segments[index + 1];
|
|
1114
|
+
return !(next !== undefined && RETRY_RESOLUTION.test(next));
|
|
1115
|
+
})
|
|
1116
|
+
.join(" ");
|
|
1117
|
+
}
|
|
1083
1118
|
function stripNegatedNonBlockerPhrases(text) {
|
|
1084
1119
|
return text
|
|
1085
1120
|
.replace(/\bno\s+(?:real\s+|remaining\s+|actual\s+)?(?:blocker|blockers|blocking issue|blocking issues|error|errors|failure|failures)\s+(?:was\s+|were\s+)?(?:encountered|observed|found|hit|seen|reported|detected)\b/g, "")
|
|
@@ -1108,9 +1143,23 @@ function traceHasStopWhenMatch(session) {
|
|
|
1108
1143
|
* the goal AND the run's own stop predicate did NOT fire. A matched stopWhen is independent,
|
|
1109
1144
|
* structured completion evidence, so it overrides a text scan of the free-form narrative — which can
|
|
1110
1145
|
* otherwise trip on the subject app's OWN quoted copy (e.g. a relayed "cannot be undone" banner).
|
|
1111
|
-
*
|
|
1146
|
+
* Resolved-arc segments never block the verdict (#453). Returns the offending reason, or undefined
|
|
1147
|
+
* when the lane is a clean pass. Exported for testing.
|
|
1112
1148
|
*/
|
|
1113
1149
|
export function resolveSelfReportedBlocker(session) {
|
|
1150
|
+
return session?.completionReason === "goal_satisfied"
|
|
1151
|
+
&& completionReasonBlocksVerdict(session.reason)
|
|
1152
|
+
&& !traceHasStopWhenMatch(session)
|
|
1153
|
+
? session.reason
|
|
1154
|
+
: undefined;
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* The friction read of the same narrative (#453): everything the verdict scan counts PLUS
|
|
1158
|
+
* resolved arcs — a participant who hit a wall, got past it, and said so has reported friction
|
|
1159
|
+
* worth a tally count and a feedback candidate, without costing the lane its pass. Same
|
|
1160
|
+
* quoted-copy and stopWhen discipline as the verdict resolver. Exported for testing.
|
|
1161
|
+
*/
|
|
1162
|
+
export function resolveSelfReportedFriction(session) {
|
|
1114
1163
|
return session?.completionReason === "goal_satisfied"
|
|
1115
1164
|
&& completionReasonContradictsGoal(session.reason)
|
|
1116
1165
|
&& !traceHasStopWhenMatch(session)
|
|
@@ -1498,7 +1547,10 @@ export async function runCuaLane(spec, deps) {
|
|
|
1498
1547
|
}),
|
|
1499
1548
|
...(deps.onObservedUrl === undefined ? {} : { onObservedUrl: deps.onObservedUrl }),
|
|
1500
1549
|
...(deps.onMessage === undefined ? {} : { onMessage: deps.onMessage }),
|
|
1501
|
-
...(deps.onScreenshot === undefined ? {} : { onScreenshot: deps.onScreenshot })
|
|
1550
|
+
...(deps.onScreenshot === undefined ? {} : { onScreenshot: deps.onScreenshot }),
|
|
1551
|
+
...(deps.onTrace === undefined
|
|
1552
|
+
? {}
|
|
1553
|
+
: { onTrace: (items) => deps.onTrace?.(spec.laneId, items) })
|
|
1502
1554
|
};
|
|
1503
1555
|
session = await deps.runSession(sessionOptions);
|
|
1504
1556
|
}
|
|
@@ -1663,6 +1715,7 @@ export async function runCuaLane(spec, deps) {
|
|
|
1663
1715
|
}
|
|
1664
1716
|
const blockerReason = resolveSelfReportedBlocker(session);
|
|
1665
1717
|
const selfReportedBlocker = blockerReason !== undefined;
|
|
1718
|
+
const reportedFriction = resolveSelfReportedFriction(session) !== undefined;
|
|
1666
1719
|
if (selfReportedBlocker) {
|
|
1667
1720
|
warnings.push(`Actor returned goal_satisfied while its final message describes a blocker or asks for missing instructions — NOT counted as a pass: ${redactText(deps.scrubKnownValues(blockerReason))}`);
|
|
1668
1721
|
}
|
|
@@ -1684,6 +1737,7 @@ export async function runCuaLane(spec, deps) {
|
|
|
1684
1737
|
warnings,
|
|
1685
1738
|
noEngagement,
|
|
1686
1739
|
selfReportedBlocker,
|
|
1740
|
+
reportedFriction,
|
|
1687
1741
|
harnessError,
|
|
1688
1742
|
...(failureCode === undefined ? {} : { failureCode }),
|
|
1689
1743
|
...(commsArtifactPath === undefined ? {} : { commsArtifactPath })
|
|
@@ -1732,6 +1786,7 @@ async function runInProcessLane(spec, deps) {
|
|
|
1732
1786
|
}
|
|
1733
1787
|
const blockerReason = resolveSelfReportedBlocker(session);
|
|
1734
1788
|
const selfReportedBlocker = blockerReason !== undefined;
|
|
1789
|
+
const reportedFriction = resolveSelfReportedFriction(session) !== undefined;
|
|
1735
1790
|
if (selfReportedBlocker) {
|
|
1736
1791
|
warnings.push(`Actor returned goal_satisfied while its final message describes a blocker or asks for missing instructions — NOT counted as a pass: ${blockerReason}`);
|
|
1737
1792
|
}
|
|
@@ -1747,6 +1802,7 @@ async function runInProcessLane(spec, deps) {
|
|
|
1747
1802
|
warnings,
|
|
1748
1803
|
noEngagement,
|
|
1749
1804
|
selfReportedBlocker,
|
|
1805
|
+
reportedFriction,
|
|
1750
1806
|
harnessError: sessionError !== undefined || session?.completionReason === "harness_error",
|
|
1751
1807
|
entryKind: "local-app"
|
|
1752
1808
|
};
|
|
@@ -1821,6 +1877,7 @@ export async function runCuaLanes(laneSpecs, deps, concurrency, runLane = runCua
|
|
|
1821
1877
|
warnings: [],
|
|
1822
1878
|
noEngagement: false,
|
|
1823
1879
|
selfReportedBlocker: false,
|
|
1880
|
+
reportedFriction: false,
|
|
1824
1881
|
harnessError: true,
|
|
1825
1882
|
sessionError: `lane runner threw outside the session guard: ${detail}`
|
|
1826
1883
|
};
|
|
@@ -2246,7 +2303,13 @@ export async function runCuaActorLab(options) {
|
|
|
2246
2303
|
return fail("HUMANISH_CUA_LAB_SUBJECT_INVALID", `local-tree packing failed: ${redactText(scrubKnownValues(toErrorMessage(error)))}`, descriptor.id);
|
|
2247
2304
|
}
|
|
2248
2305
|
}
|
|
2306
|
+
// Live-trace flush seam (#441): assigned by the attached-Observer block below when a live
|
|
2307
|
+
// run has an in-progress bundle to grow; lanes call it through deps.onTrace. Declared here
|
|
2308
|
+
// (before deps) so deps can reference it as a stable indirection.
|
|
2309
|
+
let flushLiveTrace;
|
|
2310
|
+
let stopLiveFlush;
|
|
2249
2311
|
const deps = {
|
|
2312
|
+
onTrace: (laneId, items) => flushLiveTrace?.(laneId, items),
|
|
2250
2313
|
config,
|
|
2251
2314
|
descriptor,
|
|
2252
2315
|
appUrl,
|
|
@@ -2335,6 +2398,79 @@ export async function runCuaActorLab(options) {
|
|
|
2335
2398
|
"Live CUA Observer is attached before final verification; stream auth URLs are runtime-only and are not persisted."
|
|
2336
2399
|
]);
|
|
2337
2400
|
await options.onObserverReady(liveObserver);
|
|
2401
|
+
// Incremental live flush (#441): as each lane's loop reports its recorded-so-far items,
|
|
2402
|
+
// rewrite the in-progress bundle with per-stream `liveActor` partials so the attached
|
|
2403
|
+
// Observer's 5s poll sees the timeline grow. Throttled (one write per interval, trailing
|
|
2404
|
+
// write guaranteed), serialized (never two writers), and CLOSED before the final artifact
|
|
2405
|
+
// write so a stale flush can never resurrect the in-progress bundle. A flush failure is
|
|
2406
|
+
// swallowed: mid-run observability must never break the run itself.
|
|
2407
|
+
const streamIdByLane = new Map(laneSpecs.map((spec) => [spec.laneId, spec.streamId]));
|
|
2408
|
+
const liveItemsByStream = new Map();
|
|
2409
|
+
let flushWriting;
|
|
2410
|
+
let flushDirty = false;
|
|
2411
|
+
let flushClosed = false;
|
|
2412
|
+
let flushTimer;
|
|
2413
|
+
let lastFlushAtMs = 0;
|
|
2414
|
+
const FLUSH_MIN_INTERVAL_MS = 2_000;
|
|
2415
|
+
const flushNow = async () => {
|
|
2416
|
+
while (flushDirty && !flushClosed) {
|
|
2417
|
+
flushDirty = false;
|
|
2418
|
+
lastFlushAtMs = Date.now();
|
|
2419
|
+
const updatedAt = new Date(lastFlushAtMs).toISOString();
|
|
2420
|
+
const patched = {
|
|
2421
|
+
...inProgressBundle,
|
|
2422
|
+
streams: inProgressBundle.streams.map((stream) => {
|
|
2423
|
+
const liveItems = liveItemsByStream.get(stream.id);
|
|
2424
|
+
return liveItems === undefined
|
|
2425
|
+
? stream
|
|
2426
|
+
: { ...stream, liveActor: { schema: "humanish.live-actor.v1", updatedAt, items: [...liveItems] } };
|
|
2427
|
+
})
|
|
2428
|
+
};
|
|
2429
|
+
try {
|
|
2430
|
+
await writeCuaRunArtifacts(patched, createdAt, runPaths);
|
|
2431
|
+
}
|
|
2432
|
+
catch {
|
|
2433
|
+
// Swallowed by design; the final write is the evidence of record.
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
flushWriting = undefined;
|
|
2437
|
+
};
|
|
2438
|
+
const scheduleFlush = () => {
|
|
2439
|
+
if (flushClosed || flushWriting !== undefined)
|
|
2440
|
+
return;
|
|
2441
|
+
const sinceMs = Date.now() - lastFlushAtMs;
|
|
2442
|
+
if (sinceMs >= FLUSH_MIN_INTERVAL_MS) {
|
|
2443
|
+
flushWriting = flushNow();
|
|
2444
|
+
return;
|
|
2445
|
+
}
|
|
2446
|
+
if (flushTimer === undefined) {
|
|
2447
|
+
flushTimer = setTimeout(() => {
|
|
2448
|
+
flushTimer = undefined;
|
|
2449
|
+
scheduleFlush();
|
|
2450
|
+
}, FLUSH_MIN_INTERVAL_MS - sinceMs);
|
|
2451
|
+
flushTimer.unref?.();
|
|
2452
|
+
}
|
|
2453
|
+
};
|
|
2454
|
+
flushLiveTrace = (laneId, items) => {
|
|
2455
|
+
// An empty snapshot (the initial observation on a frameless route) carries no
|
|
2456
|
+
// evidence worth a disk write; the first real item triggers the first flush.
|
|
2457
|
+
if (items.length === 0)
|
|
2458
|
+
return;
|
|
2459
|
+
const streamId = streamIdByLane.get(laneId);
|
|
2460
|
+
if (streamId === undefined)
|
|
2461
|
+
return;
|
|
2462
|
+
liveItemsByStream.set(streamId, items.slice());
|
|
2463
|
+
flushDirty = true;
|
|
2464
|
+
scheduleFlush();
|
|
2465
|
+
};
|
|
2466
|
+
stopLiveFlush = async () => {
|
|
2467
|
+
flushClosed = true;
|
|
2468
|
+
if (flushTimer !== undefined) {
|
|
2469
|
+
clearTimeout(flushTimer);
|
|
2470
|
+
flushTimer = undefined;
|
|
2471
|
+
}
|
|
2472
|
+
await flushWriting;
|
|
2473
|
+
};
|
|
2338
2474
|
}
|
|
2339
2475
|
// Run lanes (dry-run runs none). In-process is always one lane.
|
|
2340
2476
|
let outcomes;
|
|
@@ -2352,6 +2488,10 @@ export async function runCuaActorLab(options) {
|
|
|
2352
2488
|
failFastReason = ran.failFastReason;
|
|
2353
2489
|
}
|
|
2354
2490
|
}
|
|
2491
|
+
// Close the live flush BEFORE any final artifact work: no new flush may start, and an
|
|
2492
|
+
// in-flight one is awaited, so the final bundle write can never race a stale in-progress
|
|
2493
|
+
// rewrite (which would resurrect `liveActor` after completion).
|
|
2494
|
+
await stopLiveFlush?.();
|
|
2355
2495
|
const externalCommsWarnings = [];
|
|
2356
2496
|
// Adopter-hosted drain (#380): once per RUN, after every lane finished — the catch is one
|
|
2357
2497
|
// shared external endpoint, not a per-sandbox file. Same routing and digest-only artifact as
|
|
@@ -3172,7 +3312,7 @@ export function participantFeedbackCandidates(args) {
|
|
|
3172
3312
|
const session = lane.session;
|
|
3173
3313
|
if (session === undefined)
|
|
3174
3314
|
continue;
|
|
3175
|
-
const friction =
|
|
3315
|
+
const friction = resolveSelfReportedFriction(session);
|
|
3176
3316
|
const abandoned = session.status === "abandoned";
|
|
3177
3317
|
if (friction === undefined && !abandoned)
|
|
3178
3318
|
continue;
|
|
@@ -3873,7 +4013,7 @@ export function buildCuaFanoutBundle(args) {
|
|
|
3873
4013
|
? tallyParticipantOutcomes(terminalOutcomes.map((outcome) => outcome.session.status),
|
|
3874
4014
|
// A participant who reached the goal AND told you the road there was broken is the most
|
|
3875
4015
|
// useful result a study produces; reporting only the outcome would bury it.
|
|
3876
|
-
terminalOutcomes.map((outcome) => outcome.
|
|
4016
|
+
terminalOutcomes.map((outcome) => outcome.reportedFriction === true))
|
|
3877
4017
|
: undefined;
|
|
3878
4018
|
// The study funnel: per-task completion rates across every session that measured one. This is
|
|
3879
4019
|
// "where did people get stuck" as data, next to WHO got stuck (participants) above.
|