humanish 0.111.4 → 0.112.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 (93) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/actors/computer-use/openai-conversation.d.ts +91 -0
  3. package/dist/actors/computer-use/openai-conversation.js +209 -0
  4. package/dist/actors/computer-use/openai-provider.js +25 -41
  5. package/dist/actors/contract.d.ts +2 -0
  6. package/dist/analysis/diagnostics.js +7 -12
  7. package/dist/analysis/execute.d.ts +1 -1
  8. package/dist/analysis/execute.js +35 -5
  9. package/dist/analysis/service.js +7 -1
  10. package/dist/analysis/types.d.ts +23 -0
  11. package/dist/analysis/validation.d.ts +80 -35
  12. package/dist/analysis/validation.js +58 -11
  13. package/dist/cli/commands/runs.js +4 -10
  14. package/dist/cli/findings.d.ts +34 -3
  15. package/dist/cli/findings.js +136 -18
  16. package/dist/cli/io.d.ts +2 -1
  17. package/dist/cli/io.js +3 -2
  18. package/dist/feedback/draft.js +14 -4
  19. package/dist/observer/run-routes.js +8 -4
  20. package/dist/observer-app.html +4 -4
  21. package/dist/routes/computer-use/bundle.d.ts +1 -1
  22. package/dist/routes/computer-use/bundle.js +6 -16
  23. package/dist/routes/computer-use/fanout-bundle.js +40 -37
  24. package/dist/routes/computer-use/fanout-records.d.ts +2 -0
  25. package/dist/routes/computer-use/fanout-records.js +18 -26
  26. package/dist/routes/computer-use/participant-execution.js +4 -11
  27. package/dist/routes/computer-use/participant-facts.d.ts +2 -2
  28. package/dist/routes/computer-use/participant-facts.js +10 -1
  29. package/dist/routes/computer-use/result.js +7 -15
  30. package/dist/routes/computer-use/single-bundle.d.ts +2 -0
  31. package/dist/routes/computer-use/single-bundle.js +81 -37
  32. package/dist/routes/scripted/bundle.d.ts +1 -1
  33. package/dist/routes/scripted/bundle.js +9 -19
  34. package/dist/routes/scripted/surface-records.d.ts +0 -1
  35. package/dist/routes/scripted/surface-records.js +27 -13
  36. package/dist/routes/shared-world/bundle-records.d.ts +2 -2
  37. package/dist/routes/shared-world/bundle-records.js +21 -28
  38. package/dist/routes/shared-world/bundle.d.ts +1 -1
  39. package/dist/routes/shared-world/bundle.js +70 -78
  40. package/dist/routes/shared-world/participant-specs.d.ts +1 -2
  41. package/dist/routes/shared-world/participant-specs.js +0 -8
  42. package/dist/routes/shared-world/result.js +8 -12
  43. package/dist/routes/terminal/bundle.d.ts +1 -1
  44. package/dist/routes/terminal/bundle.js +10 -19
  45. package/dist/routes/terminal/dry-run.js +1 -0
  46. package/dist/routes/terminal/live-sandbox.js +9 -2
  47. package/dist/routes/terminal/plan.js +1 -0
  48. package/dist/routes/terminal/runtime.d.ts +1 -0
  49. package/dist/routes/terminal/runtime.js +1 -0
  50. package/dist/routes/terminal/session.js +1 -0
  51. package/dist/run/costs.js +9 -20
  52. package/dist/run/detail.d.ts +1 -1
  53. package/dist/run/detail.js +10 -1
  54. package/dist/run/display.d.ts +2 -2
  55. package/dist/run/display.js +15 -18
  56. package/dist/run/dry-run.js +10 -25
  57. package/dist/run/judge.d.ts +24 -0
  58. package/dist/run/judge.js +54 -0
  59. package/dist/run/liveness.d.ts +18 -0
  60. package/dist/run/liveness.js +24 -0
  61. package/dist/run/outcomes.d.ts +8 -1
  62. package/dist/run/outcomes.js +58 -12
  63. package/dist/run/participant-caption.d.ts +30 -0
  64. package/dist/run/participant-caption.js +50 -0
  65. package/dist/run/participant-records.d.ts +1 -1
  66. package/dist/run/review-markdown.d.ts +11 -0
  67. package/dist/run/review-markdown.js +22 -0
  68. package/dist/run/run-cost.d.ts +7 -0
  69. package/dist/run/run-cost.js +25 -5
  70. package/dist/run/run-index.d.ts +4 -50
  71. package/dist/run/run-index.js +57 -87
  72. package/dist/run/run.d.ts +2 -2
  73. package/dist/run/run.js +5 -3
  74. package/dist/run/stats.js +2 -12
  75. package/dist/run/transient-comms-secrets.d.ts +8 -3
  76. package/dist/run/transient-comms-secrets.js +21 -1
  77. package/dist/study/keys.d.ts +1 -0
  78. package/dist/study/keys.js +1 -1
  79. package/dist/study/parse/execution.js +7 -1
  80. package/dist/study/plan-types.d.ts +1 -0
  81. package/dist/study/types.d.ts +4 -1
  82. package/dist/study/warnings.d.ts +2 -1
  83. package/dist/study/warnings.js +3 -2
  84. package/dist/tui-app.js +73 -73
  85. package/dist/verify/liveness.d.ts +2 -2
  86. package/dist/verify/liveness.js +7 -4
  87. package/docs/architecture/terminal-product-route.md +14 -0
  88. package/docs/contracts/run-bundle.md +10 -0
  89. package/docs/contracts/schemas.md +9 -1
  90. package/docs/contracts/study-analysis.md +71 -22
  91. package/package.json +2 -1
  92. package/dist/actors/computer-use/openai-requests.d.ts +0 -62
  93. package/dist/actors/computer-use/openai-requests.js +0 -144
@@ -45,5 +45,5 @@ interface CuaRunBundleState {
45
45
  */
46
46
  export declare function buildCuaRunBundle(base: CuaRunBundleBase, state: CuaRunBundleState): RunBundle;
47
47
  /** The run's review.md: title, run, mode, gate, summary, subject, actor evidence and gaps. */
48
- export declare function renderCuaReviewMarkdown(bundle: RunBundle): string;
48
+ export declare function renderCuaReviewMarkdown(bundle: RunBundle, status?: unknown): string;
49
49
  export {};
@@ -2,8 +2,8 @@
2
2
  // and buildCuaRunBundle sends it to the single-participant or the fan-out builder with that
3
3
  // shape's arguments. renderCuaReviewMarkdown writes review.md. shared-world/bundle.ts holds the
4
4
  // same for that route.
5
- import { judgeOneParticipant, judgeParticipants, verdictText, } from "../../run/judge.js";
6
- import { reviewOutcome } from "../../run/display.js";
5
+ import { judgeOneParticipant, judgeParticipants } from "../../run/judge.js";
6
+ import { renderReviewMarkdown } from "../../run/review-markdown.js";
7
7
  import { desktopSpanToMinutes } from "../../run/cost-summary.js";
8
8
  import { e2bDesktopTemplate } from "../../substrates/e2b/sandbox.js";
9
9
  import { providerResourcesForOutcome } from "./bundle-parts.js";
@@ -92,6 +92,7 @@ function singleParticipantArgs(base, state) {
92
92
  ...(spec.evidenceAssignment === undefined ? {} : { assignment: spec.evidenceAssignment }),
93
93
  persona: spec.persona,
94
94
  resolution: spec.planned.device.resolution,
95
+ deviceName: spec.planned.device.name,
95
96
  desktopRoute: base.plan.runner.desktop !== "in-process",
96
97
  substrate: base.plan.runner.desktop === "in-process"
97
98
  ? "local-filesystem"
@@ -151,17 +152,10 @@ function singleParticipantArgs(base, state) {
151
152
  };
152
153
  }
153
154
  /** The run's review.md: title, run, mode, gate, summary, subject, actor evidence and gaps. */
154
- export function renderCuaReviewMarkdown(bundle) {
155
+ export function renderCuaReviewMarkdown(bundle, status) {
155
156
  const trace = bundle.streams[0]?.actor;
156
157
  const provenance = bundle.events.find((event) => event.type === "cua-lab.subject.provenance");
157
- return [
158
- `# ${bundle.scenario.title}`,
159
- "",
160
- `- run: ${bundle.runId}`,
161
- `- mode: ${bundle.mode}`,
162
- `- verdict: ${verdictText(bundle.review.verdict, bundle.mode)}`,
163
- `- outcome: ${reviewOutcome(bundle)}`,
164
- `- summary: ${bundle.review.summary}`,
158
+ return renderReviewMarkdown(bundle, [
165
159
  ...(provenance ? [`- subject: ${provenance.message}`] : []),
166
160
  ...(trace
167
161
  ? [
@@ -173,9 +167,5 @@ export function renderCuaReviewMarkdown(bundle) {
173
167
  : "screenshot")}`,
174
168
  ]
175
169
  : []),
176
- ...(bundle.review.gaps.length > 0
177
- ? ["", "## Gaps", ...bundle.review.gaps.map((gap) => `- ${gap}`)]
178
- : []),
179
- "",
180
- ].join("\n");
170
+ ], { status });
181
171
  }
@@ -3,11 +3,11 @@ import { receivingPublication } from "../../comms/receiving-runtime.js";
3
3
  import { actorEnding } from "../../actors/stop-cause.js";
4
4
  import { redactText } from "../../evidence/redaction.js";
5
5
  import { REVIEW_SCHEMA, bundleArtifacts, bundleHead, } from "../../run/bundle.js";
6
- import { aggregateTaskFunnels, formatParticipantOutcomes, formatRunTaskFunnel, tallyParticipantOutcomes, withCuaReviewProvenance, } from "../../run/outcomes.js";
6
+ import { aggregateTaskFunnels, formatParticipantOutcomes, formatRunTaskFunnel, gapsListClause, withCuaReviewProvenance, } from "../../run/outcomes.js";
7
7
  import { providerResourcesForOutcome, publicSafeAppUrlLabel } from "./bundle-parts.js";
8
8
  import { participantFeedbackCandidates } from "./participant-feedback.js";
9
9
  import { participantFactsOf } from "./participant-facts.js";
10
- import { participantPassed, participantStatus } from "../../run/judge.js";
10
+ import { judgeParticipantRecords } from "../../run/judge.js";
11
11
  import { buildRunCostSummary, desktopSpanToMinutes } from "../../run/cost-summary.js";
12
12
  import { formatParticipantPlanEntry } from "./participant-runs.js";
13
13
  import { fanoutParticipantRecords } from "./fanout-records.js";
@@ -32,29 +32,32 @@ function fanoutPlanEvents(args) {
32
32
  });
33
33
  return events;
34
34
  }
35
- function fanoutReview(args, streams) {
36
- const { specs, outcomes } = args;
35
+ /**
36
+ * The run's summary in plain sentences: who took part, what happened to them with the denominator
37
+ * attached, and that the gaps list the participants who did not pass.
38
+ */
39
+ function fanoutSummary(args, facts) {
40
+ const count = args.specs.length;
41
+ const people = plural(count, "participant");
42
+ const rerun = args.rerun === undefined ? "" : `Rerun from ${args.rerun.sourceRunId}: `;
43
+ if (args.inProgress === true) {
44
+ return `${people} ${count === 1 ? "is" : "are"} each using their own copy of the app. The run is still going, so nothing here is final.`;
45
+ }
46
+ if (args.dryRun) {
47
+ return `${rerun}Dry run: ${people} would each use their own copy of ${publicSafeAppUrlLabel(args.appUrl)}. No desktops were launched and $0 was spent.`;
48
+ }
49
+ const tasks = facts.tasks === undefined ? "" : ` Tasks: ${facts.tasks}.`;
50
+ return `${rerun}${people} took part, each in their own copy of the app: ${facts.outcomes}${gapsListClause(count - facts.passed, count)}.${tasks}`;
51
+ }
52
+ function fanoutReview(args, streams, judgment) {
53
+ const { outcomes } = args;
37
54
  // The judge's verdict: live fan-out must prove every participant (judgeParticipants).
38
55
  const verdict = args.verdict;
39
- const passedParticipants = (outcomes ?? []).filter((outcome) => participantPassed(participantFactsOf(outcome))).length;
56
+ const passedParticipants = judgment.participants.filter((participant) => participant.judgedStatus === "passed").length;
40
57
  // What happened to the participants, with the denominator attached. The verdict above has to
41
58
  // collapse the run to one word; this does not (docs/principles/three-roles.md).
42
59
  const terminalOutcomes = (outcomes ?? []).filter((outcome) => outcome?.session?.status !== undefined);
43
- const participants = terminalOutcomes.length > 0
44
- ? tallyParticipantOutcomes(
45
- // A no-engagement participant is not one who reached the goal. It said "done" having
46
- // taken zero actions and said nothing, and `passedParticipants` above already refuses to
47
- // count it, but `reachedGoal` was reading the trace status directly, so one run could be both
48
- // "not a passed participant" and "1/1 reached the goal". The headline number a researcher reads
49
- // first was the dishonest one. Found by a provider bug that ended a study on turn one.
50
- terminalOutcomes.map((outcome) => participantStatus(outcome.session.status, {
51
- noEngagement: outcome.noEngagement === true,
52
- selfReportedBlocker: outcome.selfReportedBlocker === true,
53
- })),
54
- // A participant who reached the goal and told you the road there was broken is the most
55
- // useful result a study produces; reporting only the outcome would bury it.
56
- terminalOutcomes.map((outcome) => outcome.reportedFriction === true))
57
- : undefined;
60
+ const participants = judgment.tally.total > 0 ? judgment.tally : undefined;
58
61
  // The study funnel: per-task completion rates across every session that measured one. This is
59
62
  // "where did people get stuck" as data, next to who got stuck (participants) above.
60
63
  const participantFunnels = (outcomes ?? [])
@@ -75,25 +78,18 @@ function fanoutReview(args, streams) {
75
78
  ...(runTasks === undefined ? {} : { tasks: runTasks }),
76
79
  // The app URL is named as the participant records name it: one on an E2B host names a
77
80
  // sandbox, so the summary carries its digest, which verify accepts in a shared run.
78
- summary: args.inProgress === true
79
- ? `Live computer-use fan-out is running (${specs.length} participants, one world each); terminal participant evidence has not been written yet.`
80
- : args.dryRun
81
- ? `${args.rerun ? `Rerun from ${args.rerun.sourceRunId}: ` : ""}Dry-run fan-out: ${specs.length} participants composed for ${args.descriptor.id} against ${publicSafeAppUrlLabel(args.appUrl)}, one world each; no desktops launched, $0 spend.`
82
- : `${args.rerun ? `Rerun from ${args.rerun.sourceRunId}: ` : ""}Computer-use fan-out (${specs.length} participants, one world each): ${passedParticipants}/${plural(specs.length, "participant")} reached a terminal, engaged verdict${participants ? `: ${formatParticipantOutcomes(participants, participantEndings)}` : ""}${runTasks ? `; tasks: ${formatRunTaskFunnel(runTasks)}` : ""}.`,
81
+ summary: fanoutSummary(args, {
82
+ passed: passedParticipants,
83
+ outcomes: participants === undefined
84
+ ? `0/${args.specs.length} finished a session`
85
+ : formatParticipantOutcomes(participants, participantEndings),
86
+ ...(runTasks === undefined ? {} : { tasks: formatRunTaskFunnel(runTasks) }),
87
+ }),
83
88
  gaps: args.inProgress === true
84
89
  ? ["Live fan-out session is still running."]
85
90
  : args.dryRun
86
91
  ? ["Live fan-out session not yet run (dry run only)."]
87
- : specs
88
- .map((spec, index) => ({ spec, outcome: outcomes?.[index] }))
89
- .filter(({ outcome }) => outcome === undefined ||
90
- outcome.skippedReason !== undefined ||
91
- outcome.sessionError !== undefined ||
92
- outcome.noEngagement ||
93
- outcome.selfReportedBlocker ||
94
- outcome.session === undefined ||
95
- outcome.session.status !== "passed")
96
- .map(({ spec, outcome }) => `${spec.planned.id}: ${outcome?.skippedReason ?? outcome?.sessionError ?? outcome?.session?.reason ?? "did not pass"}`),
92
+ : judgment.participants.flatMap((participant) => participant.gapLine === undefined ? [] : [participant.gapLine]),
97
93
  }, streams);
98
94
  return review;
99
95
  }
@@ -175,6 +171,13 @@ export function buildCuaFanoutBundle(args) {
175
171
  const simulations = [];
176
172
  const streams = [];
177
173
  const events = fanoutPlanEvents(args);
174
+ const judgment = judgeParticipantRecords(specs.map((spec, index) => ({
175
+ ...participantFactsOf(outcomes?.[index]),
176
+ id: spec.planned.id,
177
+ inProgress: args.inProgress === true && outcomes?.[index] === undefined,
178
+ })), {
179
+ runningReason: "Live computer-use participant is running; stream auth URL is available only through the attached Observer server.",
180
+ });
178
181
  let eventSeq = 2;
179
182
  const nextEventId = (suffix) => `event-${String(eventSeq++).padStart(3, "0")}-${suffix}`;
180
183
  if (args.rerun) {
@@ -187,7 +190,7 @@ export function buildCuaFanoutBundle(args) {
187
190
  });
188
191
  }
189
192
  specs.forEach((spec, index) => {
190
- const records = fanoutParticipantRecords({ args, nextEventId }, spec, index);
193
+ const records = fanoutParticipantRecords({ args, nextEventId, participants: judgment.participants }, spec, index);
191
194
  simulations.push(records.simulation);
192
195
  streams.push(records.stream);
193
196
  events.push(...records.events);
@@ -201,7 +204,7 @@ export function buildCuaFanoutBundle(args) {
201
204
  message: `Fan-out fail-fast: ${args.failFastReason}. In-flight participants finished; queued participants were skipped (blocked); completed evidence is retained.`,
202
205
  });
203
206
  }
204
- const review = fanoutReview(args, streams);
207
+ const review = fanoutReview(args, streams, judgment);
205
208
  const anyRaw = (outcomes ?? []).some((outcome) => outcome.session?.trace.redaction.screenshots === "raw");
206
209
  const ranLive = (outcomes ?? []).some((outcome) => outcome.session !== undefined || outcome.sessionError !== undefined);
207
210
  const desktopTemplate = e2bDesktopTemplate(plan.residual);
@@ -1,9 +1,11 @@
1
1
  import type { RunEvent, RunSimulation } from "../../run/bundle.js";
2
2
  import type { RunStream } from "../../run/streams.js";
3
+ import type { judgeParticipantRecords } from "../../run/judge.js";
3
4
  import type { CuaFanoutBundleArgs, DesktopParticipantRun } from "./types.js";
4
5
  /** What every participant's records share. */
5
6
  export interface FanoutParticipantContext {
6
7
  args: CuaFanoutBundleArgs;
8
+ participants: ReturnType<typeof judgeParticipantRecords>["participants"];
7
9
  /** Numbers events in the order they are pushed. */
8
10
  nextEventId: (suffix: string) => string;
9
11
  }
@@ -2,13 +2,13 @@
2
2
  // subject provenance event and the events of its outcome (session, geometry warnings, phase trail).
3
3
  import { participantAssignment } from "../../study/participant-assignment.js";
4
4
  import { participantEvent, participantRecord, participantStream, } from "../../run/participant-records.js";
5
+ import { participantCaption } from "../../run/participant-caption.js";
6
+ import { sessionEndingInWords } from "../../run/outcomes.js";
5
7
  import { declaredScreenForRender } from "../../substrates/e2b/desktop-geometry.js";
6
8
  import { describeSubjectState, phaseEventIdSuffix, publicSafeAppUrlLabel } from "./bundle-parts.js";
7
- import { judgedStatus } from "../../run/judge.js";
8
- import { participantFactsOf } from "./participant-facts.js";
9
9
  import { participantSubjectEnv } from "./types.js";
10
10
  const describeEnvNames = (names) => names.length > 0 ? names.join(", ") : "none";
11
- function fanoutParticipantView(args, spec, index) {
11
+ function fanoutParticipantView(args, spec, index, participant) {
12
12
  const { outcomes, plan } = args;
13
13
  const outcome = outcomes?.[index];
14
14
  const targetUrl = spec.planned.targetUrl ?? args.appUrl;
@@ -27,21 +27,7 @@ function fanoutParticipantView(args, spec, index) {
27
27
  };
28
28
  const screenshots = outcome?.screenshots ?? [];
29
29
  const lastScreenshot = screenshots[screenshots.length - 1];
30
- const status = args.inProgress === true && outcome === undefined
31
- ? "running"
32
- : outcome?.skippedReason !== undefined
33
- ? "blocked"
34
- : session
35
- ? session.status
36
- : outcome?.sessionError !== undefined
37
- ? "failed"
38
- : "contract_proof_only";
39
- const reason = args.inProgress === true && outcome === undefined
40
- ? "Live computer-use participant is running; stream auth URL is available only through the attached Observer server."
41
- : (outcome?.skippedReason ??
42
- session?.reason ??
43
- outcome?.sessionError ??
44
- "Dry run: the evidence shape was written without launching a desktop or spending provider tokens.");
30
+ const { status, reason } = participant;
45
31
  const traceScreenshotMode = session?.trace.redaction.screenshots;
46
32
  const screenshotMode = traceScreenshotMode === "raw" || traceScreenshotMode === "blurred"
47
33
  ? traceScreenshotMode
@@ -50,6 +36,7 @@ function fanoutParticipantView(args, spec, index) {
50
36
  : "raw";
51
37
  return {
52
38
  outcome,
39
+ participant,
53
40
  publicTargetUrl,
54
41
  subject,
55
42
  session,
@@ -64,6 +51,7 @@ function fanoutParticipantView(args, spec, index) {
64
51
  function fanoutParticipantRecord(args, spec, index, view) {
65
52
  const { plan } = args;
66
53
  const { outcome, publicTargetUrl, session, status, reason } = view;
54
+ const name = participantCaption({ id: spec.planned.id, personaId: spec.persona.id });
67
55
  return participantRecord(spec, index + 1, {
68
56
  personaId: spec.persona.id,
69
57
  scenarioId: `cua-${plan.studyId}`,
@@ -73,14 +61,14 @@ function fanoutParticipantRecord(args, spec, index, view) {
73
61
  progress: args.inProgress === true && outcome === undefined ? 20 : 100,
74
62
  currentStep: reason,
75
63
  summary: session
76
- ? `Participant ${spec.planned.id} (${spec.persona.id}/${spec.planned.device.name}): computer-use actor (${args.descriptor.id}) drove the subject app; ${session.completionReason}.`
64
+ ? `${name} used the app and ${sessionEndingInWords(session.trace)}.`
77
65
  : args.inProgress === true && outcome === undefined
78
- ? `Participant ${spec.planned.id} (${spec.persona.id}/${spec.planned.device.name}): computer-use actor (${args.descriptor.id}) is driving the subject app.`
66
+ ? `${name} is using the app.`
79
67
  : outcome?.skippedReason !== undefined
80
- ? `Participant ${spec.planned.id} ${outcome.skippedReason}.`
68
+ ? `${name} was ${outcome.skippedReason}.`
81
69
  : outcome?.sessionError !== undefined
82
- ? `Participant ${spec.planned.id} failed before a terminal session verdict: ${outcome.sessionError}`
83
- : `Participant ${spec.planned.id} (${spec.persona.id}/${spec.planned.device.name}) for ${args.descriptor.id} against ${publicTargetUrl}; no session ran.`,
70
+ ? `${name} did not finish a session: ${outcome.sessionError}`
71
+ : `${name} would use ${publicTargetUrl}; no session ran.`,
84
72
  startedAt: args.run.createdAt,
85
73
  updatedAt: args.run.createdAt,
86
74
  });
@@ -88,7 +76,7 @@ function fanoutParticipantRecord(args, spec, index, view) {
88
76
  function fanoutParticipantStream(args, spec, view) {
89
77
  const { outcome, publicTargetUrl, session, desktopGeometry, screenshots } = view;
90
78
  const { lastScreenshot, status, reason, screenshotMode } = view;
91
- const judged = outcome === undefined ? undefined : judgedStatus(participantFactsOf(outcome));
79
+ const judged = view.participant.judgedStatus;
92
80
  return participantStream(spec, {
93
81
  ...(spec.evidenceAssignment === undefined
94
82
  ? {}
@@ -103,7 +91,11 @@ function fanoutParticipantStream(args, spec, view) {
103
91
  ? {}
104
92
  : { caseGroup: spec.planned.labels.caseGroup }),
105
93
  kind: "browser",
106
- label: `${spec.planned.id} · browser`,
94
+ label: participantCaption({
95
+ id: spec.planned.id,
96
+ personaId: spec.persona.id,
97
+ device: spec.planned.device,
98
+ }),
107
99
  status,
108
100
  ...(judged === undefined ? {} : { judgedStatus: judged }),
109
101
  transport: "snapshot",
@@ -298,7 +290,7 @@ function fanoutOutcomeEvents(ctx, spec, view) {
298
290
  }
299
291
  /** One participant's simulation, stream and events, in the order the bundle records them. */
300
292
  export function fanoutParticipantRecords(ctx, spec, index) {
301
- const view = fanoutParticipantView(ctx.args, spec, index);
293
+ const view = fanoutParticipantView(ctx.args, spec, index, ctx.participants[index]);
302
294
  return {
303
295
  simulation: fanoutParticipantRecord(ctx.args, spec, index, view),
304
296
  stream: fanoutParticipantStream(ctx.args, spec, view),
@@ -1,3 +1,4 @@
1
+ import { judgeParticipantRecords } from "../../run/judge.js";
1
2
  import { createE2BParticipantDesktop } from "./e2b-desktop/desktop.js";
2
3
  import { createInProcessDesktop } from "./in-process-desktop.js";
3
4
  import path from "node:path";
@@ -6,7 +7,7 @@ import { mapWithConcurrency } from "../../run/concurrency.js";
6
7
  import { assertScreenshotEvidence, stripPngMetadataChunks } from "../../evidence/image.js";
7
8
  import { redactText, toErrorMessage } from "../../evidence/redaction.js";
8
9
  import { assertSafeOutputPathSegment, writeContainedOutputFile, } from "../../run/contained-output.js";
9
- import { participantOutcomeOk } from "./participant-facts.js";
10
+ import { participantOutcomeOk, participantFactsOf } from "./participant-facts.js";
10
11
  import { closeParticipantModel, judgeParticipantSession, participantSessionOptions, recordParticipantTrace, startParticipantModel, } from "./participant-model.js";
11
12
  /** Build a participant's writeScreenshot closure: writes under screenshots/<screenshotDir>/ and records
12
13
  * the relative path the trace references (screenshots/<name> at N=1; screenshots/<laneId>/<name>
@@ -302,16 +303,8 @@ export function toParticipantResult(spec, outcome, subject, dryRun) {
302
303
  : {
303
304
  error: {
304
305
  code: outcome.failureCode ?? "HUMANISH_COMPUTER_USE_FAILED",
305
- message: outcome.sessionError ??
306
- (outcome.noEngagement
307
- ? "Actor took no actions and produced no message (likely a blank/still-loading screen); not a credible goal_satisfied."
308
- : outcome.selfReportedBlocker
309
- ? "Actor reported goal_satisfied while its final message described a blocker or asked for missing instructions; not a credible pass."
310
- : session?.completionReason === "harness_error"
311
- ? `Computer-use session ended with a harness error: ${session.reason}`
312
- : session?.status !== "passed"
313
- ? `Computer-use session ended with ${session?.status ?? "unknown"}: ${session?.reason ?? "no terminal reason"}`
314
- : "The computer-use run did not produce a terminal session."),
306
+ message: judgeParticipantRecords([participantFactsOf(outcome)]).participants[0]
307
+ .notPassedMessage,
315
308
  },
316
309
  }),
317
310
  };
@@ -1,7 +1,7 @@
1
- import { type ExecutionFailure, type ParticipantFacts } from "../../run/judge.js";
1
+ import { type ExecutionFailure, type ParticipantRecordFacts } from "../../run/judge.js";
2
2
  import type { ParticipantRunOutcome } from "./types.js";
3
3
  /** A participant outcome's facts for the judge. */
4
- export declare function participantFactsOf(outcome: ParticipantRunOutcome): ParticipantFacts;
4
+ export declare function participantFactsOf(outcome: ParticipantRunOutcome | undefined): ParticipantRecordFacts;
5
5
  /** Whether a participant passed. A dry-run participant passes as a contract. */
6
6
  export declare function participantOutcomeOk(outcome: ParticipantRunOutcome | undefined, dryRun: boolean): boolean;
7
7
  /**
@@ -4,10 +4,19 @@
4
4
  import { participantHarnessFailed, participantPassed, sandboxCleanupFailure, } from "../../run/judge.js";
5
5
  /** A participant outcome's facts for the judge. */
6
6
  export function participantFactsOf(outcome) {
7
+ if (outcome === undefined)
8
+ return { skipped: false, noEngagement: false, selfReportedBlocker: false };
7
9
  return {
10
+ id: outcome.spec.planned.id,
11
+ ...(outcome.skippedReason === undefined ? {} : { skippedReason: outcome.skippedReason }),
12
+ reportedFriction: outcome.reportedFriction === true,
8
13
  ...(outcome.session === undefined
9
14
  ? {}
10
- : { status: outcome.session.status, completionReason: outcome.session.completionReason }),
15
+ : {
16
+ status: outcome.session.status,
17
+ completionReason: outcome.session.completionReason,
18
+ reason: outcome.session.reason,
19
+ }),
11
20
  ...(outcome.sessionError === undefined ? {} : { sessionError: outcome.sessionError }),
12
21
  skipped: outcome.skippedReason !== undefined,
13
22
  noEngagement: outcome.noEngagement === true,
@@ -1,8 +1,8 @@
1
1
  import { adapterScoreFailureMessage, applyBrowserScorer } from "../../study/adapter-extension.js";
2
2
  import { redactText } from "../../evidence/redaction.js";
3
- import { foldScorerFailures, judgeExecution, OUTCOME_POLICIES, resultOk, } from "../../run/judge.js";
3
+ import { foldScorerFailures, judgeExecution, judgeParticipantRecords, OUTCOME_POLICIES, resultOk, } from "../../run/judge.js";
4
4
  import { validatePreparedRunArtifactPaths } from "../../run/paths.js";
5
- import { participantExecutionFailures, participantOutcomeOk } from "./participant-facts.js";
5
+ import { participantExecutionFailures, participantOutcomeOk, participantFactsOf, } from "./participant-facts.js";
6
6
  import { participantCapWarning } from "./participant-model.js";
7
7
  import { summarizeCuaDiagnostics } from "./diagnostics.js";
8
8
  import { toParticipantResult } from "./participant-execution.js";
@@ -90,19 +90,11 @@ function cuaStudyResult(args) {
90
90
  message: outcome?.sessionError ??
91
91
  outcome?.providerCleanupError ??
92
92
  outcome?.providerPolicyError ??
93
- (outcome?.noEngagement
94
- ? "Actor took no actions and produced no message (likely a blank/still-loading screen); not a credible goal_satisfied."
95
- : // The participant result (toParticipantResult) named this refusal; the N=1 envelope fell through to
96
- // "did not produce a terminal session", which is false: it produced one and refused it.
97
- outcome?.selfReportedBlocker
98
- ? "Actor reported goal_satisfied while its final message described a blocker or asked for missing instructions; not a credible pass."
99
- : observer.ok
100
- ? outcome?.session?.completionReason === "harness_error"
101
- ? `Computer-use session ended with a harness error: ${outcome.session.reason}`
102
- : outcome?.session?.status !== "passed"
103
- ? `Computer-use session ended with ${outcome?.session?.status ?? "unknown"}: ${outcome?.session?.reason ?? "no terminal reason"}`
104
- : "The computer-use run did not produce a terminal session."
105
- : (observer.error?.message ?? "Observer failed for the computer-use run.")),
93
+ (outcome?.noEngagement || outcome?.selfReportedBlocker || observer.ok
94
+ ? (participantResults[0]?.error?.message ??
95
+ judgeParticipantRecords([participantFactsOf(outcome)]).participants[0]
96
+ .notPassedMessage)
97
+ : (observer.error?.message ?? "Observer failed for the computer-use run.")),
106
98
  };
107
99
  }
108
100
  const failing = (outcomes ?? []).find((outcome) => !participantOk(outcome));
@@ -32,6 +32,8 @@ export declare function buildSingleParticipantBundle(args: {
32
32
  assignment?: RunStream["assignment"];
33
33
  persona: ActorPersonaRef;
34
34
  resolution: [number, number];
35
+ /** The device preset's name; the caption names a tablet. */
36
+ deviceName?: string;
35
37
  /** False only for the custom in-process route, which has no hosted screen/window to claim. */
36
38
  desktopRoute?: boolean;
37
39
  /** The participant's runner; see runnerSubstrate for the default. */
@@ -2,10 +2,12 @@ import { participantAssignment } from "../../study/participant-assignment.js";
2
2
  import { redactText } from "../../evidence/redaction.js";
3
3
  import { REVIEW_SCHEMA, bundleArtifacts, bundleHead, } from "../../run/bundle.js";
4
4
  import {} from "../../run/streams.js";
5
- import { aggregateTaskFunnels, tallyParticipantOutcomes, withCuaReviewProvenance, } from "../../run/outcomes.js";
5
+ import { aggregateTaskFunnels, formatParticipantOutcomes, formatRunTaskFunnel, sessionEndingInWords, withCuaReviewProvenance, } from "../../run/outcomes.js";
6
+ import { participantCaption } from "../../run/participant-caption.js";
7
+ import { actorEnding } from "../../actors/stop-cause.js";
6
8
  import { describeSubjectState, phaseEventIdSuffix, publicSafeAppUrlLabel, subjectProvenanceMessage, } from "./bundle-parts.js";
7
9
  import { participantFeedbackCandidates } from "./participant-feedback.js";
8
- import { participantStatus as participantStatusFor } from "../../run/judge.js";
10
+ import { judgeParticipantRecords } from "../../run/judge.js";
9
11
  import { buildRunCostSummary } from "../../run/cost-summary.js";
10
12
  import { participantEvent, participantIds, participantRecord, participantStream, } from "../../run/participant-records.js";
11
13
  import { plural } from "../../run/text.js";
@@ -43,18 +45,25 @@ function runCost(args) {
43
45
  }
44
46
  /** The participant's status, reason, last frame, geometry and screenshot mode, shared by its records. */
45
47
  function participantView(args, publicAppUrl) {
46
- const status = args.inProgress === true
47
- ? "running"
48
- : args.session
49
- ? args.session.status
50
- : args.sessionError !== undefined
51
- ? "failed"
52
- : "contract_proof_only";
53
- const reason = args.inProgress === true
54
- ? "Live computer-use session is running; stream auth URL is available only through the attached Observer server."
55
- : (args.session?.reason ??
56
- args.sessionError ??
57
- "Dry run: the evidence shape was written without launching a desktop or spending provider tokens.");
48
+ const judgment = judgeParticipantRecords([
49
+ {
50
+ ...(args.session === undefined
51
+ ? {}
52
+ : {
53
+ status: args.session.status,
54
+ completionReason: args.session.completionReason,
55
+ reason: args.session.reason,
56
+ }),
57
+ ...(args.sessionError === undefined ? {} : { sessionError: args.sessionError }),
58
+ inProgress: args.inProgress === true,
59
+ skipped: false,
60
+ noEngagement: args.credibility?.noEngagement === true,
61
+ selfReportedBlocker: args.credibility?.selfReportedBlocker === true,
62
+ reportedFriction: args.credibility?.reportedFriction === true,
63
+ },
64
+ ]);
65
+ const participant = judgment.participants[0];
66
+ const { status, reason } = participant;
58
67
  const lastScreenshot = args.screenshots[args.screenshots.length - 1];
59
68
  const desktopGeometry = args.desktopRoute === false
60
69
  ? undefined
@@ -70,6 +79,8 @@ function participantView(args, publicAppUrl) {
70
79
  : (args.captureRedaction ?? "raw");
71
80
  return {
72
81
  publicAppUrl,
82
+ participant,
83
+ tally: judgment.tally,
73
84
  status,
74
85
  reason,
75
86
  lastScreenshot,
@@ -80,6 +91,10 @@ function participantView(args, publicAppUrl) {
80
91
  }
81
92
  function singleSimulation(args, view) {
82
93
  const { publicAppUrl, status, reason } = view;
94
+ const name = participantCaption({
95
+ id: args.participantId ?? "lane-01",
96
+ personaId: args.persona.id,
97
+ });
83
98
  return participantRecord(SINGLE, 1, {
84
99
  personaId: args.persona.id,
85
100
  scenarioId: `cua-${args.studyId}`,
@@ -89,23 +104,19 @@ function singleSimulation(args, view) {
89
104
  progress: args.inProgress === true ? 20 : 100,
90
105
  currentStep: reason,
91
106
  summary: args.session
92
- ? `Computer-use actor (${args.actorId}) drove the subject app ${browserPlace(args)}; ${args.session.completionReason}.`
107
+ ? `${name} used the app ${browserPlace(args)}, and ${sessionEndingInWords(args.session.trace)}.`
93
108
  : args.inProgress === true
94
- ? `Computer-use actor (${args.actorId}) is driving the subject app ${browserPlace(args)}.`
109
+ ? `${name} is using the app ${browserPlace(args)}.`
95
110
  : args.sessionError !== undefined
96
- ? `The computer-use run failed before a terminal session verdict: ${args.sessionError}`
97
- : `Computer-use actor (${args.actorId}) against ${publicAppUrl}; no session ran.`,
111
+ ? `${name} did not finish a session: ${args.sessionError}`
112
+ : `${name} would use ${publicAppUrl}; no session ran.`,
98
113
  startedAt: args.run.createdAt,
99
114
  updatedAt: args.run.createdAt,
100
115
  });
101
116
  }
102
117
  function singleStream(args, view) {
103
118
  const { publicAppUrl, status, reason, lastScreenshot, desktopGeometry, screenshotMode } = view;
104
- // The judge's status for a finished session, as the review counts it, so the Observer shows
105
- // the participant the way the verdict judged it.
106
- const judged = args.session === undefined || args.inProgress === true
107
- ? undefined
108
- : participantStatusFor(args.session.status, args.credibility);
119
+ const judged = args.session === undefined ? undefined : view.participant.judgedStatus;
109
120
  return participantStream(SINGLE, {
110
121
  ...(args.assignment === undefined
111
122
  ? {}
@@ -114,7 +125,13 @@ function singleStream(args, view) {
114
125
  ...(args.surface === undefined ? {} : { surface: args.surface }),
115
126
  ...(args.caseGroup === undefined ? {} : { caseGroup: args.caseGroup }),
116
127
  kind: "browser",
117
- label: `${args.participantId ?? "lane-01"} · browser`,
128
+ label: participantCaption({
129
+ id: args.participantId ?? "lane-01",
130
+ personaId: args.persona.id,
131
+ ...(args.isMobile === undefined
132
+ ? {}
133
+ : { device: { name: args.deviceName ?? "", preset: { isMobile: args.isMobile } } }),
134
+ }),
118
135
  status,
119
136
  ...(judged === undefined ? {} : { judgedStatus: judged }),
120
137
  transport: "snapshot",
@@ -257,37 +274,64 @@ function singleEvents(args, view) {
257
274
  }
258
275
  return events;
259
276
  }
277
+ /**
278
+ * The run's summary in plain sentences. A finished session gives the outcome with its denominator
279
+ * of one, any reason the harness did not count a pass, and how the session ended. The single
280
+ * participant has no gap line, so the summary carries the reason.
281
+ */
282
+ function singleSummary(args, view, facts) {
283
+ if (args.inProgress === true) {
284
+ return "1 participant is using the app. The run is still going, so nothing here is final.";
285
+ }
286
+ if (args.dryRun) {
287
+ return `Dry run: 1 participant would use ${view.publicAppUrl}. Nothing was launched and $0 was spent.`;
288
+ }
289
+ if (args.session === undefined) {
290
+ return args.sessionError === undefined
291
+ ? `1 participant was planned against ${view.publicAppUrl}, but no session ran.`
292
+ : `1 participant was planned, but the run failed before their session finished: ${view.reason}`;
293
+ }
294
+ const ending = actorEnding(args.session.trace);
295
+ const outcomes = formatParticipantOutcomes(view.tally, [
296
+ { status: args.session.status, ...(ending === undefined ? {} : { label: ending.label }) },
297
+ ]);
298
+ return [
299
+ `1 participant took part: ${outcomes}.`,
300
+ facts.tasks === undefined ? undefined : `Tasks: ${formatRunTaskFunnel(facts.tasks)}.`,
301
+ facts.credibilityNote,
302
+ `How it ended: ${view.reason}`,
303
+ ]
304
+ .filter((sentence) => sentence !== undefined)
305
+ .join(" ");
306
+ }
260
307
  function singleReview(args, view, stream) {
261
- const { reason } = view;
262
308
  // A funnel with a denominator of one is still the funnel, and its absence stays recorded: no
263
309
  // declared protocol (or a dry run) means no `tasks` field, never an empty one.
264
310
  const singleRunTasks = args.inProgress !== true && args.session?.trace.taskFunnel !== undefined
265
311
  ? aggregateTaskFunnels([args.session.trace.taskFunnel])
266
312
  : undefined;
267
- // What happened to the participant, as the route judged it: the same rule the fan-out roll-up
268
- // applies (participantStatus in src/run/judge.ts). Reading the actor's own status instead would write up
269
- // a run the route refused as "not a credible pass" as verdict pass, 1/1 reached the goal, and
270
- // every projection of the bundle (Observer tally, `runs`, the status index) would repeat it.
271
- const participantStatus = args.session === undefined
272
- ? undefined
273
- : participantStatusFor(args.session.status, args.credibility);
274
- const credibilityNote = args.session === undefined || participantStatus === args.session.status
313
+ const credibilityNote = args.inProgress === true || args.session?.status !== "passed"
275
314
  ? undefined
276
315
  : args.credibility?.noEngagement === true
277
316
  ? "Not counted as a pass: the participant took no actions and said nothing."
278
- : "Not counted as a pass: the participant's final message described a blocker.";
317
+ : args.credibility?.selfReportedBlocker === true
318
+ ? "Not counted as a pass: the participant's final message described a blocker."
319
+ : undefined;
279
320
  const review = withCuaReviewProvenance({
280
321
  schema: REVIEW_SCHEMA,
281
322
  verdict: args.verdict,
282
323
  // One participant is still a study with a denominator of one, and saying so keeps a
283
324
  // single-participant result from being read as though it generalized.
284
- ...(participantStatus !== undefined && args.inProgress !== true
325
+ ...(args.session !== undefined && args.inProgress !== true
285
326
  ? {
286
- participants: tallyParticipantOutcomes([participantStatus], [args.credibility?.reportedFriction === true]),
327
+ participants: view.tally,
287
328
  }
288
329
  : {}),
289
330
  ...(singleRunTasks === undefined ? {} : { tasks: singleRunTasks }),
290
- summary: credibilityNote === undefined ? reason : `${credibilityNote} ${reason}`,
331
+ summary: singleSummary(args, view, {
332
+ ...(singleRunTasks === undefined ? {} : { tasks: singleRunTasks }),
333
+ ...(credibilityNote === undefined ? {} : { credibilityNote }),
334
+ }),
291
335
  gaps: args.session || args.sessionError !== undefined
292
336
  ? []
293
337
  : args.inProgress === true
@@ -36,5 +36,5 @@ interface ScriptedBundleArgs {
36
36
  * the bundle-builder tests.
37
37
  */
38
38
  export declare function buildScriptedStudyBundle(args: ScriptedBundleArgs): RunBundle;
39
- export declare function renderScriptedReviewMarkdown(bundle: RunBundle): string;
39
+ export declare function renderScriptedReviewMarkdown(bundle: RunBundle, status?: unknown): string;
40
40
  export {};