humanish 0.100.1 → 0.102.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 (123) hide show
  1. package/README.md +3 -2
  2. package/dist/actor-contract.d.ts +9 -9
  3. package/dist/actor-contract.js +15 -5
  4. package/dist/actor-contract.js.map +1 -1
  5. package/dist/affordance.d.ts +2 -0
  6. package/dist/affordance.js +3 -0
  7. package/dist/affordance.js.map +1 -1
  8. package/dist/browser-control-client.d.ts +2 -0
  9. package/dist/browser-control-client.js +4 -0
  10. package/dist/browser-control-client.js.map +1 -1
  11. package/dist/browser-control-dispatcher.js +3 -0
  12. package/dist/browser-control-dispatcher.js.map +1 -1
  13. package/dist/browser-control-protocol.d.ts +77 -0
  14. package/dist/browser-control-protocol.js +31 -4
  15. package/dist/browser-control-protocol.js.map +1 -1
  16. package/dist/computer-use.d.ts +22 -2
  17. package/dist/computer-use.js +144 -32
  18. package/dist/computer-use.js.map +1 -1
  19. package/dist/cua-actor-lab.d.ts +1 -0
  20. package/dist/cua-actor-lab.js +44 -21
  21. package/dist/cua-actor-lab.js.map +1 -1
  22. package/dist/cua-speech.d.ts +13 -0
  23. package/dist/cua-speech.js +2 -0
  24. package/dist/cua-speech.js.map +1 -0
  25. package/dist/desktop-session.js +1 -0
  26. package/dist/desktop-session.js.map +1 -1
  27. package/dist/doctor-lab.js +1 -1
  28. package/dist/doctor-lab.js.map +1 -1
  29. package/dist/e2b-cua-desktop.js +51 -42
  30. package/dist/e2b-cua-desktop.js.map +1 -1
  31. package/dist/e2b-cua-provisioning.d.ts +4 -1
  32. package/dist/e2b-cua-provisioning.js +8 -2
  33. package/dist/e2b-cua-provisioning.js.map +1 -1
  34. package/dist/e2b-desktop-executor.js +3 -0
  35. package/dist/e2b-desktop-executor.js.map +1 -1
  36. package/dist/e2b-desktop-launch.d.ts +11 -1
  37. package/dist/e2b-desktop-launch.js.map +1 -1
  38. package/dist/e2b-desktop-media.d.ts +14 -0
  39. package/dist/e2b-desktop-media.js +58 -0
  40. package/dist/e2b-desktop-media.js.map +1 -0
  41. package/dist/guest-bootstrap.d.ts +4 -2
  42. package/dist/guest-bootstrap.js +16 -9
  43. package/dist/guest-bootstrap.js.map +1 -1
  44. package/dist/guest-desktop-executor.js +1 -0
  45. package/dist/guest-desktop-executor.js.map +1 -1
  46. package/dist/guest-desktop-media.d.ts +33 -0
  47. package/dist/guest-desktop-media.js +281 -0
  48. package/dist/guest-desktop-media.js.map +1 -0
  49. package/dist/guest-media-config.d.ts +15 -0
  50. package/dist/guest-media-config.js +8 -0
  51. package/dist/guest-media-config.js.map +1 -0
  52. package/dist/guest-media-worker.d.ts +1 -0
  53. package/dist/guest-media-worker.js +319 -0
  54. package/dist/guest-media-worker.js.map +1 -0
  55. package/dist/guest-runtime-desktop.d.ts +3 -1
  56. package/dist/guest-runtime-desktop.js +20 -3
  57. package/dist/guest-runtime-desktop.js.map +1 -1
  58. package/dist/guest-runtime-main.js +2 -2
  59. package/dist/guest-runtime-main.js.map +1 -1
  60. package/dist/guest-runtime.d.ts +2 -1
  61. package/dist/guest-runtime.js +1 -1
  62. package/dist/guest-runtime.js.map +1 -1
  63. package/dist/lab-config.d.ts +2 -2
  64. package/dist/lab-config.js +15 -4
  65. package/dist/lab-config.js.map +1 -1
  66. package/dist/lab-summary.js +1 -1
  67. package/dist/lab-summary.js.map +1 -1
  68. package/dist/local-agent-cli.d.ts +10 -0
  69. package/dist/local-agent-cli.js +23 -7
  70. package/dist/local-agent-cli.js.map +1 -1
  71. package/dist/local-firecracker-desktop.d.ts +3 -0
  72. package/dist/local-firecracker-desktop.js +7 -1
  73. package/dist/local-firecracker-desktop.js.map +1 -1
  74. package/dist/local-firecracker-study.js +16 -2
  75. package/dist/local-firecracker-study.js.map +1 -1
  76. package/dist/local-runtime-config.js +5 -2
  77. package/dist/local-runtime-config.js.map +1 -1
  78. package/dist/local-runtime-release.d.ts +2 -0
  79. package/dist/local-runtime-release.js +15 -0
  80. package/dist/local-runtime-release.js.map +1 -1
  81. package/dist/local-runtime.d.ts +1 -0
  82. package/dist/local-runtime.js +11 -9
  83. package/dist/local-runtime.js.map +1 -1
  84. package/dist/observer-app.html +1 -1
  85. package/dist/program.js +3 -2
  86. package/dist/program.js.map +1 -1
  87. package/dist/restricted-codex-participant-policy.d.ts +12 -5
  88. package/dist/restricted-codex-participant-policy.js +24 -49
  89. package/dist/restricted-codex-participant-policy.js.map +1 -1
  90. package/dist/restricted-codex-participant-run.js +2 -1
  91. package/dist/restricted-codex-participant-run.js.map +1 -1
  92. package/dist/restricted-codex-participant.d.ts +7 -1
  93. package/dist/restricted-codex-participant.js +168 -103
  94. package/dist/restricted-codex-participant.js.map +1 -1
  95. package/dist/restricted-codex-policy.d.ts +12 -5
  96. package/dist/restricted-codex-policy.js +62 -37
  97. package/dist/restricted-codex-policy.js.map +1 -1
  98. package/dist/restricted-codex-session.d.ts +24 -3
  99. package/dist/restricted-codex-session.js +208 -51
  100. package/dist/restricted-codex-session.js.map +1 -1
  101. package/dist/restricted-codex-transport.d.ts +11 -2
  102. package/dist/restricted-codex-transport.js +57 -6
  103. package/dist/restricted-codex-transport.js.map +1 -1
  104. package/dist/run.d.ts +8 -5
  105. package/dist/run.js +6 -6
  106. package/dist/run.js.map +1 -1
  107. package/dist/study-analysis-engine.d.ts +1 -1
  108. package/dist/study-analysis-engine.js +53 -10
  109. package/dist/study-analysis-engine.js.map +1 -1
  110. package/docs/architecture/local-browser-runtime.md +3 -2
  111. package/docs/architecture/participant-media.md +90 -0
  112. package/docs/architecture/restricted-codex-analysis.md +7 -4
  113. package/docs/contracts/schemas.md +1 -1
  114. package/docs/contracts/study-analysis.md +14 -0
  115. package/docs/goals/current.md +13 -9
  116. package/docs/ramp/README.md +10 -1
  117. package/docs/release/0.101.0-codex-ui-tools.md +28 -0
  118. package/docs/release/0.102.0-participant-media.md +43 -0
  119. package/package.json +5 -4
  120. package/skills/humanish/SKILL.md +8 -5
  121. package/dist/local-agent-appserver.d.ts +0 -44
  122. package/dist/local-agent-appserver.js +0 -269
  123. package/dist/local-agent-appserver.js.map +0 -1
@@ -1,3 +1,5 @@
1
+ import { type HeardSpeech } from "./cua-speech.js";
2
+ export { CUA_SPEECH_LIMITS, type HeardSpeech } from "./cua-speech.js";
1
3
  import { type ActorCapabilities, type ActorCompletionReason, type ActorPersonaRef, type ActorStatus, type ActorTokenUsage, type ActorTrace, type ActorTraceItem, type ParticipantDeclaredOutcome, type ParticipantClosingReport } from "./actor-contract.js";
2
4
  import type { ActorExecutionProfile, ProviderRequestReceipt } from "./actor-contract.js";
3
5
  import type { RedactionHooks } from "./redaction.js";
@@ -38,6 +40,9 @@ export type CuaAction = {
38
40
  } | {
39
41
  kind: "wait";
40
42
  ms?: number;
43
+ } | {
44
+ kind: "speak";
45
+ text: string;
41
46
  } | {
42
47
  kind: "screenshot";
43
48
  };
@@ -93,6 +98,8 @@ export interface CuaObservation {
93
98
  scrollY?: number;
94
99
  title?: string;
95
100
  text?: string;
101
+ /** Finalized speech captured from the participant's speaker sink. */
102
+ heardSpeech?: HeardSpeech[];
96
103
  }
97
104
  /**
98
105
  * A safety check the model raised. The triple is preserved verbatim from the
@@ -127,6 +134,8 @@ export interface CuaTurnRequest {
127
134
  contextHint?: string;
128
135
  }
129
136
  export interface CuaTurn {
137
+ /** This action proposal yielded from a provider request that remains active. No receipt or usage is settled yet. */
138
+ providerRequestPending?: true;
130
139
  providerRequest?: ProviderRequestReceipt;
131
140
  /** Continuation handle for the next turn. */
132
141
  responseId?: string;
@@ -144,6 +153,13 @@ export interface CuaTurn {
144
153
  output?: number;
145
154
  cachedInput?: number;
146
155
  cacheWriteInput?: number;
156
+ /** Per model inference inside this provider interaction. */
157
+ turns?: Array<{
158
+ input?: number;
159
+ output?: number;
160
+ cachedInput?: number;
161
+ cacheWriteInput?: number;
162
+ }>;
147
163
  };
148
164
  /** True when the model reported a natural endpoint (no further action). */
149
165
  done: boolean;
@@ -158,10 +174,10 @@ export interface CuaTurn {
158
174
  export interface CuaProvider {
159
175
  /** Single dispatch; the promise includes owned request cleanup. No stall retry. */
160
176
  readonly requestPolicy?: "fail_closed";
161
- readonly executionProfile?: ActorExecutionProfile;
177
+ readonly executionProfile?: ActorExecutionProfile | undefined;
162
178
  readonly historyTurnsOmitted?: number;
163
179
  readonly id: string;
164
- readonly version?: string;
180
+ readonly version?: string | undefined;
165
181
  /**
166
182
  * The request settings this provider will actually send, for the trace to record. `version` says
167
183
  * WHICH model; this says how it was asked to run. Optional: a provider with no such settings
@@ -188,6 +204,8 @@ export interface CuaProvider {
188
204
  /** Latched uncertainty from hidden interactive attempts (for example, a transport retry).
189
205
  * A later success or pre-dispatch refusal cannot make earlier unreported usage complete. */
190
206
  readonly interactionUsageIncomplete?: boolean;
207
+ /** Latest known usage for the active, unsettled request. Used only for runtime spend guards. */
208
+ readonly pendingRequestUsage?: CuaTurn["usage"];
191
209
  nextTurn(req: CuaTurnRequest, signal: AbortSignal): Promise<CuaTurn>;
192
210
  /** Optional read-only closing report. Implementations must disable tools and make no retries. */
193
211
  debrief?: ((req: CuaTurnRequest, signal: AbortSignal) => Promise<CuaTurn>) | undefined;
@@ -201,6 +219,8 @@ export interface CuaExecutor {
201
219
  * legacy observation/idle stall recovery. Wrappers must preserve this value.
202
220
  */
203
221
  readonly stallRecovery?: "fail_closed";
222
+ /** True only when this executor can play a `speak` action into the participant microphone. */
223
+ readonly speechEnabled?: boolean;
204
224
  /** Capture the current desktop frame and its state signature. */
205
225
  observe(): Promise<CuaObservation>;
206
226
  /**
@@ -1,3 +1,5 @@
1
+ import { CUA_SPEECH_LIMITS } from "./cua-speech.js";
2
+ export { CUA_SPEECH_LIMITS } from "./cua-speech.js";
1
3
  import { ACTOR_TRACE_SCHEMA } from "./actor-contract.js";
2
4
  import { CuaProviderError, isCuaProviderError } from "./cua-provider-error.js";
3
5
  import { classifyCuaAction, summarizeAffordanceUse } from "./affordance.js";
@@ -20,11 +22,29 @@ export function validClosingReport(value) {
20
22
  function validTokenCount(value) {
21
23
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
22
24
  }
25
+ function normalizedUsageTurns(usage) {
26
+ if (!Array.isArray(usage.turns) || usage.turns.length === 0)
27
+ return undefined;
28
+ const fields = ["input", "output", "cachedInput", "cacheWriteInput"];
29
+ const turns = [];
30
+ for (const raw of usage.turns) {
31
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
32
+ return undefined;
33
+ if (fields.every(field => raw[field] === undefined) || fields.some(field => raw[field] !== undefined && !validTokenCount(raw[field])))
34
+ return undefined;
35
+ if ((raw.cachedInput ?? 0) + (raw.cacheWriteInput ?? 0) > (raw.input ?? 0))
36
+ return undefined;
37
+ turns.push(Object.fromEntries(fields.flatMap(field => raw[field] === undefined ? [] : [[field, raw[field]]])));
38
+ }
39
+ return fields.every(field => turns.reduce((sum, turn) => sum + (turn[field] ?? 0), 0) === (usage[field] ?? 0))
40
+ ? turns : undefined;
41
+ }
23
42
  function completeTurnUsage(usage) {
24
43
  return usage !== undefined && validTokenCount(usage.input) && validTokenCount(usage.output)
25
44
  && (usage.cachedInput === undefined || validTokenCount(usage.cachedInput))
26
45
  && (usage.cacheWriteInput === undefined || validTokenCount(usage.cacheWriteInput))
27
- && (usage.cachedInput ?? 0) + (usage.cacheWriteInput ?? 0) <= usage.input;
46
+ && (usage.cachedInput ?? 0) + (usage.cacheWriteInput ?? 0) <= usage.input
47
+ && (usage.turns === undefined || normalizedUsageTurns(usage) !== undefined);
28
48
  }
29
49
  // Waiting is a legitimate strategy, not idleness. A persona told to sign up and verify by email
30
50
  // polls its inbox — screenshot, wait, screenshot, wait — and at 6 steps that ended the session as
@@ -62,6 +82,8 @@ export function actionFingerprint(actions) {
62
82
  return `scroll@${bucket(action.x)},${bucket(action.y)}:${Math.sign(action.dx)},${Math.sign(action.dy)}`;
63
83
  case "type":
64
84
  return `type:${action.text.length}`;
85
+ case "speak":
86
+ return `speak:${action.text.length}`;
65
87
  case "keypress":
66
88
  return `keypress:${action.keys.join("+")}`;
67
89
  case "drag":
@@ -195,6 +217,8 @@ export function describeCuaAction(action) {
195
217
  return `scroll (${action.dx}, ${action.dy}) at (${action.x}, ${action.y})`;
196
218
  case "type":
197
219
  return `type [${action.text.length} chars]`;
220
+ case "speak":
221
+ return `speak [${action.text.length} chars]`;
198
222
  case "keypress":
199
223
  return `keypress ${action.keys.join("+")}`;
200
224
  case "drag":
@@ -389,9 +413,44 @@ export async function runComputerUseLoop(options) {
389
413
  let usageInput = 0;
390
414
  let usageCachedInput = 0;
391
415
  let usageCacheWriteInput = 0;
392
- // Per provider-REQUEST usage, in order (#334): the recorded fact long-context pricing tiers
393
- // need — totals alone cannot say which requests crossed the provider's threshold.
416
+ let usageOutput = 0;
417
+ let sawUsage = false;
418
+ let incompleteInteractionUsage = false;
419
+ let unreportedInteractionUsage = false;
420
+ let interactionRequestPending = false;
421
+ // Per model-inference usage, in order (#334): the recorded fact long-context pricing tiers
422
+ // need. A continuing native tool interaction may contain several inference requests.
394
423
  const usageTurns = [];
424
+ const knownPendingUsage = () => {
425
+ const usage = interactionRequestPending ? provider.pendingRequestUsage : undefined;
426
+ if (!completeTurnUsage(usage))
427
+ return undefined;
428
+ const turns = usage.turns === undefined ? undefined : normalizedUsageTurns(usage);
429
+ return { input: usage.input, output: usage.output,
430
+ ...(usage.cachedInput === undefined ? {} : { cachedInput: usage.cachedInput }),
431
+ ...(usage.cacheWriteInput === undefined ? {} : { cacheWriteInput: usage.cacheWriteInput }),
432
+ ...(turns === undefined ? {} : { turns }) };
433
+ };
434
+ const withPendingUsage = (settled) => {
435
+ const pendingUsage = knownPendingUsage();
436
+ if (pendingUsage === undefined)
437
+ return settled;
438
+ const input = (settled.input ?? 0) + pendingUsage.input;
439
+ const output = (settled.output ?? 0) + pendingUsage.output;
440
+ return {
441
+ ...settled,
442
+ input,
443
+ output,
444
+ ...((settled.cachedInput !== undefined || pendingUsage.cachedInput !== undefined)
445
+ ? { cachedInput: (settled.cachedInput ?? 0) + (pendingUsage.cachedInput ?? 0) } : {}),
446
+ ...((settled.cacheWriteInput !== undefined || pendingUsage.cacheWriteInput !== undefined)
447
+ ? { cacheWriteInput: (settled.cacheWriteInput ?? 0) + (pendingUsage.cacheWriteInput ?? 0) } : {}),
448
+ turns: [...(settled.turns ?? []), ...(pendingUsage.turns ?? [{ input: pendingUsage.input, output: pendingUsage.output,
449
+ ...(pendingUsage.cachedInput === undefined ? {} : { cachedInput: pendingUsage.cachedInput }),
450
+ ...(pendingUsage.cacheWriteInput === undefined ? {} : { cacheWriteInput: pendingUsage.cacheWriteInput }) }])],
451
+ ...(settled.total === undefined ? {} : { total: input + output })
452
+ };
453
+ };
395
454
  // The running usage snapshot both spend guards consume: totals plus the per-request ledger,
396
455
  // shaped exactly like the trace's final tokenUsage so one estimator prices both identically.
397
456
  // Unlike the persisted trace (where absent means "unreported"), this runtime callback arg
@@ -408,18 +467,16 @@ export async function runComputerUseLoop(options) {
408
467
  ...(providerRequests.length > 0 && providerRequests.every(r => r.usageComplete && completeTurnUsage(r.usage))
409
468
  ? { total: usageInput + usageOutput } : {})
410
469
  });
411
- const runningUsage = () => provider.executionProfile?.billing === "account-unknown" ? accountUsage() : ({
470
+ const runningUsage = () => withPendingUsage(provider.executionProfile?.billing === "account-unknown" ? accountUsage() : ({
412
471
  input: usageInput,
413
472
  output: usageOutput,
414
473
  cachedInput: usageCachedInput,
415
474
  cacheWriteInput: usageCacheWriteInput,
416
475
  ...(usageTurns.length > 0 ? { turns: usageTurns } : {})
417
- });
418
- let usageOutput = 0;
419
- let sawUsage = false;
420
- let incompleteInteractionUsage = false;
421
- let unreportedInteractionUsage = false;
476
+ }));
422
477
  const hasUnreportedInteractionUsage = () => unreportedInteractionUsage || provider.interactionUsageIncomplete === true;
478
+ const usageUnavailableForCap = () => incompleteInteractionUsage || unreportedInteractionUsage ||
479
+ (interactionRequestPending ? knownPendingUsage() === undefined : provider.interactionUsageIncomplete === true);
423
480
  let lastResponseId;
424
481
  let currentPhase = "initializing computer-use loop";
425
482
  let lastActionTitle;
@@ -431,9 +488,14 @@ export async function runComputerUseLoop(options) {
431
488
  counts[key] = (counts[key] ?? 0) + 1;
432
489
  };
433
490
  const recordUsage = (turn, interaction = true) => {
434
- if (interaction && (!completeTurnUsage(turn.usage) || turn.providerRequest?.usageComplete === false))
435
- incompleteInteractionUsage = true;
491
+ if (turn.providerRequestPending === true)
492
+ return;
436
493
  const raw = turn.usage;
494
+ const turns = raw?.turns === undefined ? undefined : normalizedUsageTurns(raw);
495
+ if (interaction && (!completeTurnUsage(raw) || turn.providerRequest?.usageComplete === false))
496
+ incompleteInteractionUsage = true;
497
+ if (interaction && raw?.turns !== undefined && turns === undefined)
498
+ unreportedInteractionUsage = true;
437
499
  if (raw === undefined)
438
500
  return;
439
501
  const usage = {
@@ -449,7 +511,10 @@ export async function runComputerUseLoop(options) {
449
511
  usageCachedInput += usage.cachedInput ?? 0;
450
512
  usageCacheWriteInput += usage.cacheWriteInput ?? 0;
451
513
  usageOutput += usage.output ?? 0;
452
- usageTurns.push(usage);
514
+ if (raw.turns === undefined)
515
+ usageTurns.push(usage);
516
+ else if (turns !== undefined)
517
+ usageTurns.push(...turns);
453
518
  };
454
519
  /** Marked providers own one attempt through settlement; an outer race never retries it. */
455
520
  const markedRequest = async (request, kind, capMs) => {
@@ -463,6 +528,7 @@ export async function runComputerUseLoop(options) {
463
528
  let failure;
464
529
  let failed = false;
465
530
  let accepted;
531
+ let pendingYield = false;
466
532
  const pending = Promise.resolve().then(() => {
467
533
  if (controller.signal.aborted)
468
534
  throw new CuaProviderError("cancelled", { dispatched: false, usageComplete: false, cleanup: "confirmed" });
@@ -471,9 +537,18 @@ export async function runComputerUseLoop(options) {
471
537
  void pending.catch(() => undefined);
472
538
  try {
473
539
  accepted = await raceBounded(`participant ${kind}`, pending, remaining(), capMs, signal);
474
- const r = accepted.providerRequest;
475
- if (!r || r.dispatched !== true || typeof r.usageComplete !== "boolean" || r.cleanup !== "confirmed") {
476
- throw new CuaProviderError("invalid_response", { dispatched: "unknown", usageComplete: false, cleanup: "unconfirmed" }, accepted.usage);
540
+ if (accepted.providerRequestPending === true) {
541
+ if (kind !== "interaction" || accepted.done || accepted.actions.length === 0 || accepted.pendingSafetyChecks.length > 0 ||
542
+ accepted.providerRequest !== undefined || accepted.usage !== undefined || accepted.interruption !== undefined || accepted.closingReport !== undefined) {
543
+ throw new CuaProviderError("invalid_response", { dispatched: "unknown", usageComplete: false, cleanup: "unconfirmed" });
544
+ }
545
+ pendingYield = true;
546
+ }
547
+ else {
548
+ const r = accepted.providerRequest;
549
+ if (!r || r.dispatched !== true || typeof r.usageComplete !== "boolean" || r.cleanup !== "confirmed") {
550
+ throw new CuaProviderError("invalid_response", { dispatched: "unknown", usageComplete: false, cleanup: "unconfirmed" }, accepted.usage);
551
+ }
477
552
  }
478
553
  }
479
554
  catch (error) {
@@ -495,6 +570,10 @@ export async function runComputerUseLoop(options) {
495
570
  }
496
571
  }
497
572
  }
573
+ if (!failed && pendingYield) {
574
+ interactionRequestPending = true;
575
+ return accepted;
576
+ }
498
577
  const final = settlement;
499
578
  const typed = isCuaProviderError(failure) ? failure : final && "error" in final && isCuaProviderError(final.error) ? final.error : undefined;
500
579
  const turn = final && "turn" in final ? final.turn : undefined;
@@ -502,14 +581,15 @@ export async function runComputerUseLoop(options) {
502
581
  const receipt = raw && (typeof raw.dispatched === "boolean" || raw.dispatched === "unknown") &&
503
582
  typeof raw.usageComplete === "boolean" && ["confirmed", "unconfirmed"].includes(raw.cleanup) ? { dispatched: raw.dispatched, usageComplete: raw.usageComplete, cleanup: raw.cleanup }
504
583
  : { dispatched: "unknown", usageComplete: false, cleanup: "unconfirmed" };
505
- const rawUsage = typed?.usage ?? turn?.usage;
584
+ const rawUsage = typed?.usage ?? (turn?.providerRequestPending === true ? undefined : turn?.usage);
506
585
  const usage = rawUsage === undefined ? undefined : Object.fromEntries(["input", "output", "cachedInput", "cacheWriteInput", "total"].flatMap(key => {
507
586
  const value = rawUsage[key];
508
587
  return validTokenCount(value) ? [[key, value]] : [];
509
588
  }));
510
589
  // restricted-codex-session sets dispatched only after initialize/config/account/
511
590
  // thread/MCP admission, immediately before turn/start; it is not a success claim.
512
- providerRequests.push({ ordinal: providerRequests.length + 1, kind, ...receipt,
591
+ const settledKind = interactionRequestPending ? "interaction" : kind;
592
+ providerRequests.push({ ordinal: providerRequests.length + 1, kind: settledKind, ...receipt,
513
593
  profileVerified: provider.executionProfile !== undefined && receipt.dispatched === true,
514
594
  ...(typed ? { errorCode: typed.code, ...(typed.failurePhase === undefined ? {} : { failurePhase: typed.failurePhase }) } : {}),
515
595
  ...(usage === undefined ? {} : { usage: { ...usage } }) });
@@ -518,11 +598,12 @@ export async function runComputerUseLoop(options) {
518
598
  record({ id: nextId("notice"), kind: "notice", lifecycle: "completed", status: "error",
519
599
  title: "participant request cleanup unconfirmed", text: "The request did not confirm cleanup within the settlement boundary. No further participant request or action is admitted." });
520
600
  }
521
- if (kind === "interaction" && receipt.dispatched !== false && (!receipt.usageComplete || !completeTurnUsage(usage)))
601
+ if (settledKind === "interaction" && receipt.dispatched !== false && (!receipt.usageComplete || !completeTurnUsage(usage)))
522
602
  unreportedInteractionUsage = true;
603
+ interactionRequestPending = false;
523
604
  if (failed) {
524
605
  if (usage)
525
- recordUsage({ actions: [], pendingSafetyChecks: [], done: false, usage, providerRequest: receipt }, kind === "interaction");
606
+ recordUsage({ actions: [], pendingSafetyChecks: [], done: false, usage, providerRequest: receipt }, settledKind === "interaction");
526
607
  if (failure instanceof CuaAbortError || failure instanceof CuaDeadlineError)
527
608
  throw failure;
528
609
  if (failure instanceof CuaStallError)
@@ -667,6 +748,24 @@ export async function runComputerUseLoop(options) {
667
748
  return await raceBounded(`observe (${label}, retry)`, executor.observe(), remaining(), observationTimeoutMs, signal);
668
749
  }
669
750
  };
751
+ const seenSpeechIds = new Set();
752
+ let pendingHeardSpeech = [];
753
+ let heardSpeechChanged = false;
754
+ const collectHeardSpeech = (value) => {
755
+ for (const utterance of value.heardSpeech ?? []) {
756
+ if (seenSpeechIds.has(utterance.id))
757
+ continue;
758
+ seenSpeechIds.add(utterance.id);
759
+ pendingHeardSpeech.push(utterance);
760
+ heardSpeechChanged = true;
761
+ record({ id: nextId("notice"), kind: "notice", lifecycle: "completed", status: "ok",
762
+ title: "speech heard", text: redactNarration(utterance.text) });
763
+ }
764
+ if (pendingHeardSpeech.length > CUA_SPEECH_LIMITS.utterances) {
765
+ throw new CuaExecutorError("invalid_response", "outcome_uncertain");
766
+ }
767
+ return pendingHeardSpeech.length === 0 ? value : { ...value, heardSpeech: pendingHeardSpeech.slice() };
768
+ };
670
769
  // The declared observation window (#510). The harness holds, looks, and takes nothing back to
671
770
  // the model: no action, no turn, no tokens. It runs once, cut to whatever session budget is
672
771
  // left, and says in the trace that the time was deliberate.
@@ -705,7 +804,7 @@ export async function runComputerUseLoop(options) {
705
804
  throw new CuaAbortError();
706
805
  await sleep(Math.min(dwell.everyMs, budget - (now() - dwellStartedAtMs)));
707
806
  currentPhase = `dwell frame ${frames + 1}`;
708
- const frameObservation = await observeBounded(`dwell frame ${frames + 1}`);
807
+ const frameObservation = collectHeardSpeech(await observeBounded(`dwell frame ${frames + 1}`));
709
808
  frames += 1;
710
809
  if (frameObservation.screenshot !== undefined)
711
810
  onScreenshot?.(frameObservation.screenshot);
@@ -729,7 +828,7 @@ export async function runComputerUseLoop(options) {
729
828
  contextHint = `The study held this page under observation for ${Math.round(dwellHeldMs / 1000)} seconds (a declared dwell window; you took no actions in that time). Continue the mission from the current state of the page.`;
730
829
  return "continue";
731
830
  };
732
- let observation = await observeBounded("initial");
831
+ let observation = collectHeardSpeech(await observeBounded("initial"));
733
832
  // Runtime-only: hand the seat's live location.href back to the orchestrator (never persisted).
734
833
  onObservedUrl?.(observation.url);
735
834
  if (observation.screenshot !== undefined)
@@ -744,7 +843,7 @@ export async function runComputerUseLoop(options) {
744
843
  observeTasks(observation, 0);
745
844
  const initialDwell = await dwellIfDue(observation, 0);
746
845
  if (initialDwell !== undefined) {
747
- observation = await observeBounded("after dwell");
846
+ observation = collectHeardSpeech(await observeBounded("after dwell"));
748
847
  onObservedUrl?.(observation.url);
749
848
  if (observation.screenshot !== undefined)
750
849
  onScreenshot?.(observation.screenshot);
@@ -877,9 +976,19 @@ export async function runComputerUseLoop(options) {
877
976
  }
878
977
  }
879
978
  bump("turns");
979
+ pendingHeardSpeech = [];
980
+ heardSpeechChanged = false;
880
981
  previousResponseId = turn.responseId ?? previousResponseId;
881
982
  lastResponseId = turn.responseId ?? lastResponseId;
882
983
  recordUsage(turn);
984
+ // A host-authenticated provider may learn its account billing class during
985
+ // startup. Never price that account usage from an API model rate.
986
+ if (provider.executionProfile?.billing === "account-unknown" &&
987
+ (maxUsd !== undefined || overRunBudget !== undefined || estimateTurnCostUsd !== undefined)) {
988
+ completionReason = "harness_error";
989
+ reason = "Codex is using a ChatGPT account; API dollar caps cannot bound account usage. Use a finite timeout or the OpenAI API participant.";
990
+ break;
991
+ }
883
992
  if (turn.interruption !== undefined) {
884
993
  // A provider can exhaust its response budget before producing visible text, or midway
885
994
  // through an action. Preserve usage and partial narration, but never interpret either as
@@ -909,7 +1018,7 @@ export async function runComputerUseLoop(options) {
909
1018
  title: tokenLimit ? "provider token limit reached" : unexpectedStatus ? "unexpected provider response status" : "provider response incomplete", text: reason });
910
1019
  break;
911
1020
  }
912
- if (requiresUsage && (incompleteInteractionUsage || hasUnreportedInteractionUsage())) {
1021
+ if (requiresUsage && usageUnavailableForCap()) {
913
1022
  stopForUnreportedUsage();
914
1023
  break;
915
1024
  }
@@ -1029,7 +1138,7 @@ export async function runComputerUseLoop(options) {
1029
1138
  if (taskTracker !== undefined) {
1030
1139
  try {
1031
1140
  currentPhase = "observing closing task state";
1032
- const closing = await observeBounded("closing");
1141
+ const closing = collectHeardSpeech(await observeBounded("closing"));
1033
1142
  observeTasks(closing, turnNumber);
1034
1143
  }
1035
1144
  catch (error) {
@@ -1203,6 +1312,7 @@ export async function runComputerUseLoop(options) {
1203
1312
  kind: "ui_action",
1204
1313
  lifecycle: "completed",
1205
1314
  title: actionTitle,
1315
+ ...(action.kind === "speak" ? { text: redactNarration(action.text) } : {}),
1206
1316
  // Structured pin coordinates (#441), exactly the click classes the Observer
1207
1317
  // pins render — recorded fact instead of a title re-parse downstream.
1208
1318
  ...(action.kind === "click" || action.kind === "double_click"
@@ -1213,7 +1323,7 @@ export async function runComputerUseLoop(options) {
1213
1323
  if (signal?.aborted)
1214
1324
  throw new CuaAbortError();
1215
1325
  currentPhase = `observing UI state after turn ${turnNumber}`;
1216
- observation = await observeBounded(`after turn ${turnNumber}`);
1326
+ observation = collectHeardSpeech(await observeBounded(`after turn ${turnNumber}`));
1217
1327
  // Runtime-only: hand the seat's live location.href back to the orchestrator (never persisted).
1218
1328
  onObservedUrl?.(observation.url);
1219
1329
  if (observation.screenshot !== undefined)
@@ -1228,7 +1338,7 @@ export async function runComputerUseLoop(options) {
1228
1338
  observeTasks(observation, turnNumber);
1229
1339
  const dwellOutcome = await dwellIfDue(observation, turnNumber);
1230
1340
  if (dwellOutcome !== undefined) {
1231
- observation = await observeBounded("after dwell");
1341
+ observation = collectHeardSpeech(await observeBounded("after dwell"));
1232
1342
  onObservedUrl?.(observation.url);
1233
1343
  if (observation.screenshot !== undefined)
1234
1344
  onScreenshot?.(observation.screenshot);
@@ -1276,13 +1386,14 @@ export async function runComputerUseLoop(options) {
1276
1386
  // behavioral signal already (the agent took nothing but screenshots and waits), so it keeps
1277
1387
  // reading the frame on its own — a repeated screenshot is exactly what an idle streak IS, and
1278
1388
  // feeding repetition into it would grant an extra forgiveness step for being idle.
1279
- const progressed = frameChanged || !repeatingRecentAction;
1389
+ const observedProgress = frameChanged || heardSpeechChanged;
1390
+ const progressed = observedProgress || !repeatingRecentAction;
1280
1391
  // A screenshot/wait turn while the UI visibly changes may be patience through loading or a
1281
1392
  // transition, so grant a bounded recovery window. Do not grant infinite immunity: animated
1282
1393
  // pixels or state-executor turn counters can otherwise keep a screenshot/wait loop alive
1283
1394
  // until the wall-clock timeout.
1284
1395
  if (idleThisTurn) {
1285
- if (frameChanged && idleProgressForgivenessUsed < IDLE_PROGRESS_FORGIVENESS_STEPS) {
1396
+ if (observedProgress && idleProgressForgivenessUsed < IDLE_PROGRESS_FORGIVENESS_STEPS) {
1286
1397
  idleProgressForgivenessUsed += 1;
1287
1398
  consecutiveIdle = 0;
1288
1399
  }
@@ -1434,11 +1545,11 @@ export async function runComputerUseLoop(options) {
1434
1545
  skip = "the session deadline was reached";
1435
1546
  else if (provider.requiresFrame && closingObservation.screenshot === undefined)
1436
1547
  skip = "the final observation has no required frame";
1437
- if (skip === undefined && (maxUsd !== undefined || overRunBudget !== undefined) && (incompleteInteractionUsage || hasUnreportedInteractionUsage())) {
1548
+ if (skip === undefined && (maxUsd !== undefined || overRunBudget !== undefined) && usageUnavailableForCap()) {
1438
1549
  skip = "remaining model budget is unknown because an earlier participant turn did not report complete usage";
1439
1550
  }
1440
1551
  if (skip === undefined && maxUsd !== undefined) {
1441
- const estimate = sawUsage ? estimateTurnCostUsd?.(runningUsage()) : undefined;
1552
+ const estimate = sawUsage || knownPendingUsage() !== undefined ? estimateTurnCostUsd?.(runningUsage()) : undefined;
1442
1553
  if (estimate === undefined || estimate === null || !Number.isFinite(estimate))
1443
1554
  skip = "remaining model budget could not be established";
1444
1555
  else if (estimate >= maxUsd)
@@ -1512,7 +1623,7 @@ export async function runComputerUseLoop(options) {
1512
1623
  const detail = signal?.aborted ? "cancelled" : controller.signal.aborted || error instanceof CuaDeadlineError || error instanceof CuaStallError
1513
1624
  ? "closing report deadline reached" : error instanceof Error ? error.message : String(error);
1514
1625
  const closingReceipt = provider.requestPolicy === "fail_closed" ? providerRequests.at(-1) : undefined;
1515
- const usageReported = closingReceipt?.kind === "debrief" && closingReceipt.usageComplete && completeTurnUsage(closingReceipt.usage);
1626
+ const usageReported = closingReceipt?.usageComplete === true && completeTurnUsage(closingReceipt.usage);
1516
1627
  note("failed", `${detail}; closing request usage is ${usageReported ? "reported" : "unreported"}`, usageReported);
1517
1628
  }
1518
1629
  }
@@ -1579,7 +1690,8 @@ export async function runComputerUseLoop(options) {
1579
1690
  ...(affordanceObservations.length > 0 ? { affordanceUse: summarizeAffordanceUse(affordanceObservations) } : {}),
1580
1691
  ...(declaredOutcome === undefined ? {} : { declaredOutcome }),
1581
1692
  ...(debrief === undefined ? {} : { debrief }),
1582
- ...(hasUnreportedInteractionUsage() || (requiresUsage && incompleteInteractionUsage) ? { interactionUsageIncomplete: true } : {}),
1693
+ ...(interactionRequestPending || hasUnreportedInteractionUsage() || (requiresUsage && incompleteInteractionUsage)
1694
+ ? { interactionUsageIncomplete: true } : {}),
1583
1695
  // The funnel is present exactly when a protocol was declared — including a session that ended
1584
1696
  // on turn 0, whose funnel honestly reads 0/N. No tasks declared means no funnel, not an empty one.
1585
1697
  ...(taskTracker === undefined ? {} : { taskFunnel: taskTracker.funnel() }),