humanish 0.131.1 → 0.133.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 (31) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/actors/codex/restricted-participant.d.ts +2 -0
  3. package/dist/actors/codex/restricted-participant.js +6 -2
  4. package/dist/actors/computer-use/loop/actions.js +1 -0
  5. package/dist/actors/computer-use/loop/debrief.js +63 -23
  6. package/dist/actors/computer-use/loop/types.d.ts +16 -7
  7. package/dist/actors/computer-use/loop/watch.js +17 -15
  8. package/dist/actors/computer-use/openai-context.js +26 -14
  9. package/dist/actors/computer-use/openai-conversation.d.ts +38 -8
  10. package/dist/actors/computer-use/openai-conversation.js +133 -22
  11. package/dist/actors/computer-use/openai-provider.js +11 -35
  12. package/dist/actors/computer-use/openai-wire.d.ts +21 -0
  13. package/dist/actors/computer-use/openai-wire.js +82 -9
  14. package/dist/actors/computer-use/wait.d.ts +8 -6
  15. package/dist/actors/computer-use/wait.js +10 -8
  16. package/dist/actors/contract.d.ts +4 -0
  17. package/dist/actors/local-agent/cli.js +5 -5
  18. package/dist/evidence/redaction.d.ts +2 -0
  19. package/dist/evidence/redaction.js +8 -0
  20. package/dist/evidence/sensitive-json.d.ts +9 -0
  21. package/dist/evidence/sensitive-json.js +96 -0
  22. package/dist/routes/computer-use/local-vm.js +9 -3
  23. package/dist/routes/computer-use/participant-model.js +3 -0
  24. package/dist/study/bundle-estimate.d.ts +5 -0
  25. package/dist/study/bundle-estimate.js +15 -1
  26. package/dist/study/types.d.ts +5 -4
  27. package/dist/verify/verify.js +4 -2
  28. package/docs/architecture/actor-contract.md +9 -3
  29. package/docs/architecture/browser-control.md +2 -2
  30. package/docs/contracts/schemas.md +30 -11
  31. package/package.json +1 -1
@@ -0,0 +1,96 @@
1
+ // containsSensitive over a JSON document that may not fit in one string. V8's longest string is
2
+ // 536,870,888 characters on Node 24, and a run with every participant's trace loaded can pass it.
3
+ //
4
+ // The document is scanned as pieces: the text before the array and its first element, each
5
+ // element after that, and the last element with the text after the array. Every cut then falls on
6
+ // the `,` between two objects, `},{`. Most patterns stop at a quote, so a match stays inside one
7
+ // JSON string. Those that read a name and its value cross only the quotes around its `:`. A
8
+ // credential variable's unquoted value runs over brackets and braces up to a comma, so at a cut it
9
+ // stops, but after the last element it can run over the array's `]`. That is why the text after
10
+ // the array joins the last element.
11
+ //
12
+ // The sandbox URL can cross a cut. Its user info runs over quotes, braces and commas up to an `@`:
13
+ // `https://host"},{"contact":"ops@e2b-host`. From the end of its scheme to one character past
14
+ // `e2b`, that match holds no slash and no whitespace. So a match across a cut starts at a scheme
15
+ // with no slash or whitespace after it up to the cut, and reaches the next piece only up to its
16
+ // first slash or whitespace. Between that scheme and the cut, the match can only be in its user
17
+ // info. The scheme and every `@` there are inside JSON strings, so a host that starts after one
18
+ // stops at its string's closing quote. Had that host reached `e2b` first, the scan of its piece or
19
+ // an earlier junction would have matched. So the scan keeps only the scheme between pieces. Each
20
+ // cut's junction scans the scheme, `"}` in place of the text after it, the cut's `,` and the next
21
+ // piece up to its first slash or whitespace. A public E2B page after the scheme would need a space
22
+ // or the text's end before the cut, so neither text exempts the URL. The scan holds one piece at a
23
+ // time.
24
+ import { isRecord } from "../run/type-guards.js";
25
+ import { containsSandboxUrl, containsSensitive } from "./redaction.js";
26
+ // The sandbox URL's scheme, `https://` or `https:\/\/` as JSON may escape it, at a text's end.
27
+ const SCHEME_AT_END = /https?:\\?\/\\?\/$/i;
28
+ const SLASH_OR_SPACE = /[/\s]/;
29
+ /**
30
+ * containsSensitive(JSON.stringify(value)), with each element of the array `value[key]` serialized
31
+ * and scanned on its own, so no one string holds two elements. Only an array of plain objects is
32
+ * scanned that way. Any other array, and a value, array or element with a toJSON or a prototype of
33
+ * its own, is scanned as the whole text, which can be longer than one string holds. Getters and
34
+ * proxies are not detected, so the answer is the whole text's for values as JSON.parse returns
35
+ * them.
36
+ */
37
+ export function containsSensitiveJson(value, key) {
38
+ // JSON.stringify writes only own enumerable keys, and `around` cuts the text at this one.
39
+ const elements = isPlainRecord(value) && Object.keys(value).includes(key) ? value[key] : undefined;
40
+ // Array.from reads a hole as undefined, where every would skip it.
41
+ if (!isPlainRecord(value) ||
42
+ !isPlainArray(elements) ||
43
+ !Array.from(elements).every(isPlainRecord))
44
+ return wholeTextSensitive(value);
45
+ const [prefix, suffix] = around(value, key);
46
+ if (elements.length === 0)
47
+ return containsSensitive(prefix + suffix);
48
+ // The scheme of a URL no slash or whitespace has followed, from an earlier piece.
49
+ let scheme;
50
+ for (const [index, element] of elements.entries()) {
51
+ const own = JSON.stringify(element);
52
+ const piece = (index === 0 ? prefix : "") + own + (index === elements.length - 1 ? suffix : "");
53
+ if (containsSensitive(piece))
54
+ return true;
55
+ if (scheme !== undefined && containsSandboxUrl(`${scheme}"},${head(piece)}`))
56
+ return true;
57
+ scheme = schemeAfter(scheme, piece);
58
+ }
59
+ return false;
60
+ }
61
+ const wholeTextSensitive = (value) => containsSensitive(JSON.stringify(value) ?? "");
62
+ // JSON.stringify calls a toJSON with the value's key in its parent, and an element serialized
63
+ // alone gets "" in place of its index. It writes a boxed primitive as the primitive. A plain object
64
+ // or array with no toJSON is written as its own keys or items wherever it sits.
65
+ const hasToJson = (value) => typeof value.toJSON === "function";
66
+ function isPlainRecord(value) {
67
+ if (!isRecord(value) || hasToJson(value))
68
+ return false;
69
+ const prototype = Object.getPrototypeOf(value);
70
+ return prototype === Object.prototype || prototype === null;
71
+ }
72
+ const isPlainArray = (value) => Array.isArray(value) && Object.getPrototypeOf(value) === Array.prototype && !hasToJson(value);
73
+ /** The document's text before the first element of `value[key]` and after its last. */
74
+ function around(value, key) {
75
+ const keys = Object.keys(value);
76
+ const at = keys.indexOf(key);
77
+ const entries = (from, to) => Object.fromEntries(keys.slice(from, to).map((name) => [name, value[name]]));
78
+ const before = JSON.stringify({ ...entries(0, at), [key]: [] });
79
+ const after = JSON.stringify(entries(at + 1));
80
+ return [before.slice(0, -"]}".length), after === "{}" ? "]}" : `],${after.slice(1)}`];
81
+ }
82
+ /** `piece` up to and including its first slash or whitespace, or all of it when it has none. */
83
+ function head(piece) {
84
+ const at = piece.search(SLASH_OR_SPACE);
85
+ return at === -1 ? piece : piece.slice(0, at + 1);
86
+ }
87
+ /** The scheme a sandbox URL could continue from after `piece`, given the one before it. */
88
+ function schemeAfter(before, piece) {
89
+ let last = piece.length - 1;
90
+ while (last >= 0 && !SLASH_OR_SPACE.test(piece[last]))
91
+ last -= 1;
92
+ if (last === -1)
93
+ return before;
94
+ // `https:\/\/` is the longest scheme: ten characters ending at the slash.
95
+ return SCHEME_AT_END.exec(piece.slice(Math.max(0, last - 9), last + 1))?.[0];
96
+ }
@@ -1,5 +1,6 @@
1
1
  import { collectDesktopRecording } from "../../evidence/desktop-recording-artifact.js";
2
2
  import path from "node:path";
3
+ import { participantList } from "../../study/study-fields.js";
3
4
  import { inboxRecipientFor, } from "./participant-desktop.js";
4
5
  import { createLocalFirecrackerDesktop, } from "../../substrates/local/firecracker-desktop.js";
5
6
  import { localBrowserDefaults, localBrowserUnsupportedReason, } from "../../substrates/local/runtime-config.js";
@@ -139,11 +140,16 @@ function createLocalParticipantDesktop(context, run, warnings, artifactRoot) {
139
140
  };
140
141
  }
141
142
  /** The Codex account participant, whose unconfirmed cleanup blocks the study's analysis. */
142
- function accountProvider(state, maxWaitMs) {
143
- return async ({ executor }) => {
143
+ function accountProvider(state, config) {
144
+ return async ({ executor, participant: { index } }) => {
145
+ // The wait settings the participant's loop runs with (plan-participants.ts), so its tool
146
+ // description states them.
147
+ const maxWaitMs = config.actor?.maxWaitMs;
148
+ const idleWaitMs = participantList(config)?.[index]?.idleWaitMs ?? config.actor?.idleWaitMs;
144
149
  const participant = createRestrictedCodexParticipant({
145
150
  speechEnabled: executor.speechEnabled === true,
146
151
  ...(maxWaitMs === undefined ? {} : { maxWaitMs }),
152
+ ...(idleWaitMs === undefined ? {} : { idleWaitMs }),
147
153
  });
148
154
  state.participants.push(participant);
149
155
  const provider = Object.assign(participant.provider, {
@@ -185,7 +191,7 @@ export function prepareLocalVmRun(options) {
185
191
  const { config: _config, assets: _assets, signal: _signal, ...runOptions } = options;
186
192
  return {
187
193
  options: account
188
- ? { ...runOptions, createProvider: accountProvider(state, config.actor?.maxWaitMs) }
194
+ ? { ...runOptions, createProvider: accountProvider(state, config) }
189
195
  : runOptions,
190
196
  localVm: {
191
197
  desktop: (run, warnings, artifactRoot) => createLocalParticipantDesktop(context, run, warnings, artifactRoot),
@@ -47,6 +47,9 @@ export async function startParticipantModel(spec, deps, executor) {
47
47
  ...(spec.planned.limits.maxWaitMs === undefined
48
48
  ? {}
49
49
  : { maxWaitMs: spec.planned.limits.maxWaitMs }),
50
+ ...(spec.planned.limits.idleWaitMs === undefined
51
+ ? {}
52
+ : { idleWaitMs: spec.planned.limits.idleWaitMs }),
50
53
  session: { env },
51
54
  });
52
55
  return { provider: codexParticipant.provider, codexParticipant };
@@ -16,6 +16,11 @@ export interface BundleEstimate {
16
16
  readonly runFileBytes: number;
17
17
  /** The largest participant trace file, which analysis also reads whole. */
18
18
  readonly largestTraceBytes: number;
19
+ /**
20
+ * Every participant's trace file together, whose items the run writer and the Observer page join
21
+ * in one string.
22
+ */
23
+ readonly tracesBytes: number;
19
24
  /** observer/index.html, which verify reads whole. */
20
25
  readonly observerPageBytes: number;
21
26
  /** Every file, screenshots included. */
@@ -16,6 +16,13 @@
16
16
  // observer-data.json leaves the lists out too and was 0.94 to 1.12 of run.json's size on the live
17
17
  // runs measured, so it passes 32 MiB only after run.json passes 16 MiB, and the estimate does not
18
18
  // check it.
19
+ // The Observer page and the Observer server join every participant's items in one string before
20
+ // gzip. On a hosted desktop the sandbox id scrub (publicSandboxView) does too, on every write of
21
+ // run.json and on a bundle export of the loaded run, which its 25 MiB default --max-bytes refuses
22
+ // first. V8's longest string is 536,870,888 characters
23
+ // on Node 24. A trace file's bytes are at least the characters of its items as compact JSON, so the
24
+ // estimate compares the traces' bytes and may warn a little early.
25
+ import { constants } from "node:buffer";
19
26
  import { EVIDENCE_LIMITS } from "../analysis/analysis-limits.js";
20
27
  import { RUN_ARTIFACT_MAX_BYTES } from "../run/contained-output.js";
21
28
  import { formatDuration } from "../run/projection.js";
@@ -156,6 +163,7 @@ export function estimateBundle(participants, sessionMs) {
156
163
  return {
157
164
  runFileBytes,
158
165
  largestTraceBytes: largestTrace,
166
+ tracesBytes: traces,
159
167
  observerPageBytes,
160
168
  // observer-data.json is about run.json's size.
161
169
  totalBytes: observerPageBytes + screenshots * RATES.screenshotBytes + traces + 2 * runFileBytes,
@@ -178,6 +186,7 @@ export function estimateBundle(participants, sessionMs) {
178
186
  export function bundleLimitReason(estimate) {
179
187
  const runFile = estimate.runFileBytes > EVIDENCE_LIMITS.sourceBytes;
180
188
  const page = estimate.observerPageBytes > RUN_ARTIFACT_MAX_BYTES;
189
+ const joined = estimate.tracesBytes > constants.MAX_STRING_LENGTH;
181
190
  const entries = estimate.entries > estimate.entryLimit;
182
191
  const passed = [
183
192
  ...(runFile
@@ -195,6 +204,11 @@ export function bundleLimitReason(estimate) {
195
204
  `The Observer page, observer/index.html, may grow to about ${mib(estimate.observerPageBytes)}, past the ${mib(RUN_ARTIFACT_MAX_BYTES)} verify reads in one file.`,
196
205
  ]
197
206
  : []),
207
+ ...(joined
208
+ ? [
209
+ `All participants' traces together may grow to about ${mib(estimate.tracesBytes)}, past the ${mib(constants.MAX_STRING_LENGTH)} that fits in one string. On a hosted desktop, writing run.json puts them in one string, and so does a bundle export allowed past its 25 MiB default; the Observer page always does. run.json may then stop updating, that export would fail and the page would not build.`,
210
+ ]
211
+ : []),
198
212
  ...(entries
199
213
  ? [
200
214
  `The run may hold about ${count(estimate.entries)} files and folders, past the ${count(estimate.entryLimit)} the public-safety scan reads in a run of ${plural(estimate.basis.participants, "participant")}.`,
@@ -222,7 +236,7 @@ export function bundleLimitReason(estimate) {
222
236
  ...(runFile ? ["shorten the mission (actor.mission)"] : []),
223
237
  ];
224
238
  // A trace is one participant's, so splitting the study helps only the run's own files.
225
- if (runFile || page || entries || changes.length === 0)
239
+ if (runFile || page || joined || entries || changes.length === 0)
226
240
  changes.push("split the participants across studies");
227
241
  const advice = changes.length < 2
228
242
  ? changes.join("")
@@ -414,10 +414,11 @@ export interface StudyActor {
414
414
  maxWaitMs?: number;
415
415
  /**
416
416
  * Computer-use and shared-world: how long a `wait` that names no duration lasts when the
417
- * participant's turn only waits or takes screenshots, and how often the loop looks at the screen
418
- * while it watches such a turn, in whole milliseconds from 1000 to 600000 and no longer than
419
- * maxWaitMs. Default 5000. After an action in the same turn such a wait lasts 500 ms. OpenAI's
420
- * computer-use `wait` never names a duration.
417
+ * participant's turn only waits or takes screenshots, the shortest such a turn is watched for,
418
+ * and how often the loop looks at the screen while it watches such a turn, in whole milliseconds
419
+ * from 1000 to 600000 and no longer than maxWaitMs. Default 5000. After an action in the same
420
+ * turn such a wait lasts 500 ms. OpenAI's computer tool `wait` never names a duration; its
421
+ * wait_minutes function does.
421
422
  */
422
423
  idleWaitMs?: number;
423
424
  /**
@@ -2,7 +2,8 @@ import path from "node:path";
2
2
  import { listAnalysisExecutions } from "../analysis/store-executions.js";
3
3
  import { loadAnalysis } from "../analysis/load.js";
4
4
  import { analysisSharingProblems } from "../analysis/sharing.js";
5
- import { containsSensitive, REDACTED_SANDBOX_ID } from "../evidence/redaction.js";
5
+ import { REDACTED_SANDBOX_ID } from "../evidence/redaction.js";
6
+ import { containsSensitiveJson } from "../evidence/sensitive-json.js";
6
7
  import { keyedSandboxIds, readRunSandboxIds } from "../run/sandbox-ids.js";
7
8
  import { validatePreparedRunArtifactPaths } from "../run/paths.js";
8
9
  import { isReadRunBundleSchema, RUN_BUNDLE_FILE, RUN_BUNDLE_SCHEMA, RUN_BUNDLE_V1_SCHEMA, RUN_BUNDLE_V2_SCHEMA, RUN_BUNDLE_V3_SCHEMA, } from "../run/bundle.js";
@@ -112,9 +113,10 @@ export async function verifyResolvedRun(cwd, runInput, runPaths) {
112
113
  addTraceScan(traces, publicSafetyFindings, unscannedArtifacts);
113
114
  // JSON escapes can hide a sensitive value from the byte scan while the
114
115
  // decoded recording exposes it to Observer, feedback, or analysis input.
116
+ // Each participant is scanned apart: together their traces can pass V8's longest string.
115
117
  if (publicSafetyFindings.length < MAX_REPORTED_FINDINGS &&
116
118
  recorded !== null &&
117
- containsSensitive(JSON.stringify(recorded))) {
119
+ containsSensitiveJson(recorded, "streams")) {
118
120
  publicSafetyFindings.push("sensitive decoded run.json");
119
121
  }
120
122
  checks.push({
@@ -176,6 +176,7 @@ export interface ActorTraceItem {
176
176
  looks: number;
177
177
  area?: { x: number; y: number; width: number; height: number }; // pixels, coordinates only
178
178
  };
179
+ namedMs?: number; // computer-use wait ui_action: the length the participant named, before any cap
179
180
  }
180
181
 
181
182
  export interface ActorCapabilities {
@@ -387,9 +388,14 @@ request), without provider support, or without remaining time and known budget.
387
388
  unreported usage remains an unknown cost line. `counts.debriefCalls` is separate
388
389
  from interaction turns, and reported usage contributes to aggregate cost.
389
390
 
390
- The OpenAI implementation makes one request with tools disabled and structured
391
- output; no HTTP or policy retries. Its output limit is 3072 tokens, or the declared
392
- `maxOutputTokens` when lower. Stateless/ZDR mode does not offer retrospective
391
+ The OpenAI implementation sends each closing request with tools disabled and no HTTP or
392
+ policy retries. The request sends no `text.format`, so it reads the session from the prompt
393
+ cache, and states the JSON Schema in its input. The reply's JSON object is read from its text,
394
+ also from inside a code fence or after a message of its own. A completed reply with no JSON that
395
+ schema accepts, apart from a refusal, comes back with `closingRetry` set. The loop may then call
396
+ the same method once more, after the checks the first request passed and with a time bound of
397
+ its own, and that request sets the strict schema. `counts.debriefCalls` counts each request. Its
398
+ output limit is 3072 tokens, or the declared `maxOutputTokens` when lower. Stateless/ZDR mode does not offer retrospective
393
399
  reporting: its client-carried conversation summarizes its oldest turns past a token
394
400
  budget, so it may not hold the whole session a retrospective report needs. It does offer
395
401
  the impressions-only request, which reads the carried conversation: its newest
@@ -110,8 +110,8 @@ with speech, whose heard speech reaches the participant only with a screenshot),
110
110
  shortened` notice with both durations when it does, and sends the rest as consecutive waits of at
111
111
  most 30 seconds. The Codex participant's `humanish_ui` schema leaves a wait's length open for that
112
112
  reason; the wire still refuses a single wait over 30 seconds. A wait that names no duration, as
113
- every OpenAI computer-use wait does, reaches the desktop with a length too: 500 ms after an action
114
- in the same turn. A turn that only waits or takes screenshots sends no wait to the desktop: the
113
+ the OpenAI computer tool's `wait` does, reaches the desktop with a length too: 500 ms after an
114
+ action in the same turn. A turn that only waits or takes screenshots sends no wait to the desktop: the
115
115
  loop observes the screen every `actor.idleWaitMs` until it changes or the wait's time passes
116
116
  (`src/actors/computer-use/loop/watch.ts`), so a waiting participant costs one observe request per
117
117
  look.
@@ -1082,14 +1082,17 @@ Core-owned fields:
1082
1082
  16 pixels counts once it holds still at a look; one of 24 tiles or fewer once
1083
1083
  it has held still for more than 65 s with no tile within 4 tiles of it ticking
1084
1084
  or moving with a region of more than 24 tiles
1085
- (`src/evidence/frame-change.ts`). The first watch lasts the turn's planned
1086
- length: the named waits' lengths, together up to `actor.maxWaitMs` (else
1087
- 600000), and `idleWaitMs` for each wait that names none. Each further one in a row that ended
1088
- at its limit, or that a change of 24 tiles or fewer woke, lasts twice as long,
1089
- up to the larger of its planned length and that cap. `limitMs` is how long it
1090
- could last; `askedMs` is what the turn's waits asked for, the same sum with no
1091
- cap, so it is above `limitMs` when the named waits passed the cap and below it
1092
- when the watch grew; `wake` is `changed`, `speech`,
1085
+ (`src/evidence/frame-change.ts`). The first watch's limit is the turn's
1086
+ planned length: the named waits' lengths, together up to `actor.maxWaitMs` (else
1087
+ 600000), and `idleWaitMs` for each wait that names none, and at least
1088
+ `idleWaitMs`. Each further one in a row that ended at its limit, or that a
1089
+ change of 24 tiles or fewer woke, lasts twice as long, up to the larger of its
1090
+ planned length and that cap. An OpenAI participant names a length with the
1091
+ `wait_minutes` function tool; its computer tool's `wait` names none.
1092
+ `limitMs` is how long it could last; `askedMs` is what the turn's
1093
+ waits asked for, the same sum with no cap, so it is above `limitMs` when the
1094
+ named waits passed the cap and below it when the watch grew or the named waits
1095
+ were shorter than `idleWaitMs`; `wake` is `changed`, `speech`,
1093
1096
  `condition`, `limit`, `clock` (wind-down or end), `stalled` (a look got no
1094
1097
  frame; the next observation is a fresh one), or `stopped` (the study budget or
1095
1098
  the session's time ran out); `looks` counts its looks; `area` (`x`, `y`,
@@ -1100,6 +1103,14 @@ Core-owned fields:
1100
1103
  waits after the one carrying `watch` are titled `wait 0ms`. The look that
1101
1104
  ended a watch is the turn's `turn-NN` screenshot; other looks are not kept.
1102
1105
  Additive: absent on plain waits, other producers and older bundles
1106
+ - optional `items[].namedMs`, on the computer-use `ui_action` of each wait that
1107
+ named its length: that length in milliseconds, as the participant named it,
1108
+ before `actor.maxWaitMs` or the watch's cap applied. An OpenAI participant
1109
+ names a length only through `wait_minutes`, so its waits with `namedMs` are
1110
+ that tool's calls. Its waits without it are the computer tool's `wait`, a
1111
+ `wait_minutes` call whose minutes were not a finite number above zero, and
1112
+ a call to a function it does not have. Additive: absent on waits that named
1113
+ none, other items and producers, and older bundles
1103
1114
  - optional `items[].page` and `finalPage` (`{ origin, path }`, `TracePage` in
1104
1115
  `src/actors/contract.ts`): where a computer-use participant's browser was. The loop puts `page`
1105
1116
  on the `screenshot` item of each observation that read a URL: `turn-00-start` is the page before
@@ -1183,8 +1194,9 @@ Core-owned fields:
1183
1194
  with the outputs that answered it, and the latest reply. Past an estimated
1184
1195
  64,000 input tokens it is cut down to 32,000 in one step: the opening
1185
1196
  screenshot is dropped first, then the oldest exchanges become lines of a note,
1186
- sent as an assistant message, that keeps their reasoning summaries, messages
1187
- and actions as text, with any text that came back with their screenshots.
1197
+ sent as an assistant message, that keeps their reasoning summaries, messages,
1198
+ actions and `wait_minutes` calls as text, with any text that came back with
1199
+ their screenshots or in a function call's output, such as how a wait ended.
1188
1200
  Past 32,000 characters the note keeps its first four lines and its newest
1189
1201
  ones, and names the turns between them as not listed.
1190
1202
  The two newest exchanges are always carried whole, even when they alone pass
@@ -1199,7 +1211,14 @@ Core-owned fields:
1199
1211
  `estimatedInputTokens`. The impressions-only request has `kind: impressions`;
1200
1212
  the optional `kind` field is absent on participant turns and in older bundles.
1201
1213
  Impressions carry the context the next turn would carry, with the same budget
1202
- and summarization of older turns. Billed input per request is in `tokenUsage.turns`. A
1214
+ and summarization of older turns. The closing report and impressions requests
1215
+ send no `text.format`, so they can read the session from the prompt cache; the
1216
+ request states the JSON Schema in its input instead. A completed reply with no
1217
+ JSON the schema accepts, apart from a refusal, can be sent once more with the
1218
+ strict schema, only after the budget and time checks the first passed and with
1219
+ a time bound of its own. An impressions request sent twice has two entries. The
1220
+ closing report has no entry here. Each closing request that reported usage has
1221
+ its own row in `tokenUsage.turns` after the participant's requests. Billed input per request is in `tokenUsage.turns`. A
1203
1222
  participant that ran in `explicit_context` gets a run warning. Absent on other
1204
1223
  providers and on earlier bundles
1205
1224
  - optional `affordanceUse` (`humanish.affordance-use.v1`): which kind of route this
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "humanish",
3
- "version": "0.131.1",
3
+ "version": "0.133.0",
4
4
  "description": "Synthetic user research for apps, CLIs, and agent-facing product flows.",
5
5
  "keywords": [
6
6
  "cli",