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.
- package/CHANGELOG.md +25 -0
- package/dist/actors/codex/restricted-participant.d.ts +2 -0
- package/dist/actors/codex/restricted-participant.js +6 -2
- package/dist/actors/computer-use/loop/actions.js +1 -0
- package/dist/actors/computer-use/loop/debrief.js +63 -23
- package/dist/actors/computer-use/loop/types.d.ts +16 -7
- package/dist/actors/computer-use/loop/watch.js +17 -15
- package/dist/actors/computer-use/openai-context.js +26 -14
- package/dist/actors/computer-use/openai-conversation.d.ts +38 -8
- package/dist/actors/computer-use/openai-conversation.js +133 -22
- package/dist/actors/computer-use/openai-provider.js +11 -35
- package/dist/actors/computer-use/openai-wire.d.ts +21 -0
- package/dist/actors/computer-use/openai-wire.js +82 -9
- package/dist/actors/computer-use/wait.d.ts +8 -6
- package/dist/actors/computer-use/wait.js +10 -8
- package/dist/actors/contract.d.ts +4 -0
- package/dist/actors/local-agent/cli.js +5 -5
- package/dist/evidence/redaction.d.ts +2 -0
- package/dist/evidence/redaction.js +8 -0
- package/dist/evidence/sensitive-json.d.ts +9 -0
- package/dist/evidence/sensitive-json.js +96 -0
- package/dist/routes/computer-use/local-vm.js +9 -3
- package/dist/routes/computer-use/participant-model.js +3 -0
- package/dist/study/bundle-estimate.d.ts +5 -0
- package/dist/study/bundle-estimate.js +15 -1
- package/dist/study/types.d.ts +5 -4
- package/dist/verify/verify.js +4 -2
- package/docs/architecture/actor-contract.md +9 -3
- package/docs/architecture/browser-control.md +2 -2
- package/docs/contracts/schemas.md +30 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,31 @@ The Unreleased section holds the full notes for the next version until it is tag
|
|
|
8
8
|
|
|
9
9
|
## Unreleased
|
|
10
10
|
|
|
11
|
+
## 0.133.0: An OpenAI participant names how long it waits (2026-10-14)
|
|
12
|
+
|
|
13
|
+
humanish 0.133.0 lets an OpenAI computer-use participant name how long it waits, with a
|
|
14
|
+
`wait_minutes` tool, and cuts what a participant costs while nothing on its screen changes. A turn of
|
|
15
|
+
only waits is watched, and each further quiet wait in a row lasts twice as long, starting from the
|
|
16
|
+
length the participant named, up to 10 minutes. In live 60-minute runs against 0.132.0, median model
|
|
17
|
+
spend per participant-hour fell 13% on a resident portal, 52% on a support queue, 46% on an issue
|
|
18
|
+
list that shows new issues only on reload, and 78% on a dispatch board, and all 6 participants on
|
|
19
|
+
the issue list reloaded and reported every new issue. verify and analysis also no longer fail on a
|
|
20
|
+
run whose traces together pass 512 MiB.
|
|
21
|
+
|
|
22
|
+
[Release notes](https://github.com/danielgwilson/humanish/releases/tag/v0.133.0)
|
|
23
|
+
|
|
24
|
+
## 0.132.0: An OpenAI participant's closing report reads the prompt cache (2026-10-13)
|
|
25
|
+
|
|
26
|
+
humanish 0.132.0 cuts the cost of an OpenAI computer-use participant's closing report. The closing
|
|
27
|
+
report and impressions requests set `text.format`, which is part of the prompt cache's prefix, so
|
|
28
|
+
each one was billed as a fresh write of the whole session: 25% to 42% of a run's model spend in the
|
|
29
|
+
8 runs measured on 0.131.0. They now send the schema in their input instead and read the session
|
|
30
|
+
from the cache. In two live 12-minute runs the closing request cost $0.026 and $0.025 where it
|
|
31
|
+
would have cost $0.14. A reply that does not match the schema is asked once more with the strict
|
|
32
|
+
schema, after the same budget and time checks.
|
|
33
|
+
|
|
34
|
+
[Release notes](https://github.com/danielgwilson/humanish/releases/tag/v0.132.0)
|
|
35
|
+
|
|
11
36
|
## 0.131.1: Hosted desktops keep their screens on while participants wait (2026-10-13)
|
|
12
37
|
|
|
13
38
|
humanish 0.131.1 keeps a hosted desktop's screen on while a participant waits without input.
|
|
@@ -22,6 +22,8 @@ export interface RestrictedParticipantOptions {
|
|
|
22
22
|
* defaults the loop applies without it (resolveWaitSettings).
|
|
23
23
|
*/
|
|
24
24
|
maxWaitMs?: number;
|
|
25
|
+
/** The participant's idleWaitMs, the shortest a call of only waits lasts, stated the same way. */
|
|
26
|
+
idleWaitMs?: number;
|
|
25
27
|
}
|
|
26
28
|
/** What doctor's hosted-participant check found, before any turn. */
|
|
27
29
|
export interface ParticipantReadiness {
|
|
@@ -41,7 +41,7 @@ function deferred() {
|
|
|
41
41
|
return { promise, resolve, reject };
|
|
42
42
|
}
|
|
43
43
|
// prose-check: model prompt (Codex reads this tool description, not a person)
|
|
44
|
-
const toolDescription = (speechEnabled, { maxWaitMs, watchMaxMs }) => `Act on the participant's browser through humanish. Submit one to four UI actions${speechEnabled ? ", including speak when you need to reply aloud," : ""} and a short public comment; never private reasoning. humanish returns a JSON STRING with execution acknowledgments${speechEnabled ? ", speech heard from the actual participant speaker sink," : ""} and a fresh screenshot. In Code Mode use: const r = JSON.parse(await tools.humanish_ui({narration: "...", actions: [...]})); text({acknowledgments: r.acknowledgments${speechEnabled ? ", heardSpeech: r.heardSpeech" : ""}, contextHint: r.contextHint, closing: r.closing}); image(r.imageUrl). Call serially and inspect each returned screenshot${speechEnabled ? " and heardSpeech array" : ""} before deciding what to do next. A call whose actions are only waits
|
|
44
|
+
const toolDescription = (speechEnabled, { maxWaitMs, watchMaxMs, idleWaitMs, }) => `Act on the participant's browser through humanish. Submit one to four UI actions${speechEnabled ? ", including speak when you need to reply aloud," : ""} and a short public comment; never private reasoning. humanish returns a JSON STRING with execution acknowledgments${speechEnabled ? ", speech heard from the actual participant speaker sink," : ""} and a fresh screenshot. In Code Mode use: const r = JSON.parse(await tools.humanish_ui({narration: "...", actions: [...]})); text({acknowledgments: r.acknowledgments${speechEnabled ? ", heardSpeech: r.heardSpeech" : ""}, contextHint: r.contextHint, closing: r.closing}); image(r.imageUrl). Call serially and inspect each returned screenshot${speechEnabled ? " and heardSpeech array" : ""} before deciding what to do next. A call whose actions are only waits and that asks for some wait time asks to wait its waits' ms together; an ask under ${idleWaitMs} ms counts as ${idleWaitMs} ms, and named ms count up to ${watchMaxMs} ms${speechEnabled ? ". It returns when speech is heard" : ""}. contextHint says how long it waited and why it returned. A wait among other actions lasts its ms, at most ${maxWaitMs} ms.${speechEnabled ? " Use speak only after the visible UI shows that you joined the call and your microphone is unmuted; it sends audio into that call." : ""} Acknowledged input does not prove an application outcome. If closing is true, stop calling tools and give your final account.`;
|
|
45
45
|
const participantInstructions = (prompt, speechEnabled) => `${prompt}\n\nYou are the study participant throughout this conversation, including its closing account. Use only the supplied screenshots and humanish_ui tool to interact. The tool returns a JSON string: parse it, inspect acknowledgments${speechEnabled ? " and heardSpeech captured from the actual participant speaker sink" : ""}, and display imageUrl with Code Mode image(). Do not print the image data URL as text. Keep your persona and earlier observations throughout the session.${speechEnabled ? " A speak action plays into the participant microphone; use it only after the visible UI shows that you joined the call and the microphone is unmuted." : ""} Speak publicly about your experience, never reveal private reasoning. Completed inputs do not prove application outcomes; verify on the next screenshot. When the task ends, return only the required final JSON with outcome, summary, frictionReports and impressions. Report observed confusion and recovered mistakes as well as blockers. ${IMPRESSIONS_ASK} Do not invent observations.`;
|
|
46
46
|
/** Turns and failures from the native run, handed in order to the next provider request. */
|
|
47
47
|
class ParticipantEvents {
|
|
@@ -192,7 +192,11 @@ function participantSession(options, speechEnabled, effort, call) {
|
|
|
192
192
|
reasoningEffort: effort,
|
|
193
193
|
tool: {
|
|
194
194
|
name: "humanish_ui",
|
|
195
|
-
description: toolDescription(speechEnabled, resolveWaitSettings({
|
|
195
|
+
description: toolDescription(speechEnabled, resolveWaitSettings({
|
|
196
|
+
maxWaitMs: options.maxWaitMs,
|
|
197
|
+
idleWaitMs: options.idleWaitMs,
|
|
198
|
+
speechEnabled,
|
|
199
|
+
})),
|
|
196
200
|
inputSchema: participantToolSchema(speechEnabled),
|
|
197
201
|
call,
|
|
198
202
|
},
|
|
@@ -209,6 +209,7 @@ export async function runActionBatch(session, actions, watched) {
|
|
|
209
209
|
? { coord: { x: action.x, y: action.y } }
|
|
210
210
|
: {}),
|
|
211
211
|
...(watched !== undefined && waitIndex === watched.endedOn ? { watch: watched.watch } : {}),
|
|
212
|
+
...(requested.kind === "wait" && requested.ms !== undefined ? { namedMs: requested.ms } : {}),
|
|
212
213
|
}));
|
|
213
214
|
}
|
|
214
215
|
return { execution, hint: hint() };
|
|
@@ -86,36 +86,68 @@ function planDebrief(session, ask, trigger) {
|
|
|
86
86
|
}
|
|
87
87
|
return { send, boundMs };
|
|
88
88
|
}
|
|
89
|
-
async function debriefExchange(session, context, trigger, ask,
|
|
89
|
+
async function debriefExchange(session, context, trigger, ask, plan, record) {
|
|
90
|
+
const { provider } = session;
|
|
91
|
+
const request = {
|
|
92
|
+
instructions: session.settings.instructions,
|
|
93
|
+
observation: trigger.observation,
|
|
94
|
+
...(provider.requestPolicy !== "fail_closed" || context.previousExecution === undefined
|
|
95
|
+
? {}
|
|
96
|
+
: { previousExecution: context.previousExecution }),
|
|
97
|
+
...(context.previousResponseId === undefined
|
|
98
|
+
? {}
|
|
99
|
+
: { previousResponseId: context.previousResponseId }),
|
|
100
|
+
...(context.acknowledgedSafetyChecks === undefined
|
|
101
|
+
? {}
|
|
102
|
+
: { acknowledgedSafetyChecks: context.acknowledgedSafetyChecks }),
|
|
103
|
+
contextHint: ask.contextHint,
|
|
104
|
+
};
|
|
105
|
+
const first = await sendClosing(session, ask, request, plan);
|
|
106
|
+
if (!("turn" in first)) {
|
|
107
|
+
return first.status === "skipped"
|
|
108
|
+
? record("skipped", `${first.detail}; no closing request was sent`)
|
|
109
|
+
: record("failed", first.detail, first.usageReported);
|
|
110
|
+
}
|
|
111
|
+
if (first.turn.closingRetry !== true)
|
|
112
|
+
return acceptDebriefTurn(session, first.turn, trigger, ask, record, first.usageReported);
|
|
113
|
+
// The first reply's usage is already recorded, so the checks see it. Under a cap, a reply
|
|
114
|
+
// without complete usage stops another paid request, as it does after a participant turn.
|
|
115
|
+
const again = session.capDeclared && !first.usageReported
|
|
116
|
+
? {
|
|
117
|
+
skip: "the closing request did not report complete usage, so the remaining model budget is unknown",
|
|
118
|
+
}
|
|
119
|
+
: planDebrief(session, ask, trigger);
|
|
120
|
+
const second = "skip" in again
|
|
121
|
+
? { status: "skipped", detail: again.skip }
|
|
122
|
+
: await sendClosing(session, ask, request, again);
|
|
123
|
+
if ("turn" in second) {
|
|
124
|
+
const usageReported = first.usageReported && second.usageReported;
|
|
125
|
+
return acceptDebriefTurn(session, second.turn, trigger, ask, record, usageReported, "; asked twice: the first reply was not the JSON the request asked for");
|
|
126
|
+
}
|
|
127
|
+
// The first reply stands, with why no second one replaced it.
|
|
128
|
+
const skipped = second.status === "skipped";
|
|
129
|
+
return acceptDebriefTurn(session, first.turn, trigger, ask, record, first.usageReported && (skipped || second.usageReported), `; ${skipped ? "not asked again" : "asked once more"}: ${second.detail}`);
|
|
130
|
+
}
|
|
131
|
+
/** Send one closing request within its time bound, and record the usage its reply reports. */
|
|
132
|
+
async function sendClosing(session, ask, request, { send, boundMs }) {
|
|
90
133
|
const { signal, provider } = session;
|
|
91
134
|
const scope = requestScope(signal);
|
|
92
135
|
const timer = setTimeout(() => scope.end(), boundMs);
|
|
93
136
|
timer.unref?.();
|
|
94
137
|
session.trace.bump("debriefCalls");
|
|
95
138
|
try {
|
|
96
|
-
const request = {
|
|
97
|
-
instructions: session.settings.instructions,
|
|
98
|
-
observation: trigger.observation,
|
|
99
|
-
...(provider.requestPolicy !== "fail_closed" || context.previousExecution === undefined
|
|
100
|
-
? {}
|
|
101
|
-
: { previousExecution: context.previousExecution }),
|
|
102
|
-
...(context.previousResponseId === undefined
|
|
103
|
-
? {}
|
|
104
|
-
: { previousResponseId: context.previousResponseId }),
|
|
105
|
-
...(context.acknowledgedSafetyChecks === undefined
|
|
106
|
-
? {}
|
|
107
|
-
: { acknowledgedSafetyChecks: context.acknowledgedSafetyChecks }),
|
|
108
|
-
contextHint: ask.contextHint,
|
|
109
|
-
};
|
|
110
139
|
const turn = provider.requestPolicy === "fail_closed"
|
|
111
140
|
? await singleDispatch(session, "debrief", (dispatchSignal) => send(request, dispatchSignal), boundMs)
|
|
112
141
|
: await raceSessionDeadline(send(request, scope.signal), boundMs, signal);
|
|
113
|
-
return
|
|
142
|
+
return { turn, usageReported: recordClosingUsage(session, turn) };
|
|
114
143
|
}
|
|
115
144
|
catch (error) {
|
|
116
145
|
// A failed optional report cannot rewrite the already observed structured completion.
|
|
117
146
|
if (isComputerUseAdmissionLimitError(error)) {
|
|
118
|
-
return
|
|
147
|
+
return {
|
|
148
|
+
status: "skipped",
|
|
149
|
+
detail: "the adapter reported a local admission limit before provider dispatch",
|
|
150
|
+
};
|
|
119
151
|
}
|
|
120
152
|
const detail = signal?.aborted
|
|
121
153
|
? "cancelled"
|
|
@@ -126,14 +158,19 @@ async function debriefExchange(session, context, trigger, ask, { send, boundMs }
|
|
|
126
158
|
: String(error);
|
|
127
159
|
const debriefReceipt = provider.requestPolicy === "fail_closed" ? session.usage.requests.at(-1) : undefined;
|
|
128
160
|
const usageReported = debriefReceipt?.usageComplete === true && isCompleteTurnUsage(debriefReceipt.usage);
|
|
129
|
-
return
|
|
161
|
+
return {
|
|
162
|
+
status: "failed",
|
|
163
|
+
detail: `${detail}; closing request usage is ${usageReported ? "reported" : "unreported"}`,
|
|
164
|
+
usageReported,
|
|
165
|
+
};
|
|
130
166
|
}
|
|
131
167
|
finally {
|
|
132
168
|
clearTimeout(timer);
|
|
133
169
|
scope.end();
|
|
134
170
|
}
|
|
135
171
|
}
|
|
136
|
-
|
|
172
|
+
/** Record a closing reply's usage, and say whether it was complete. */
|
|
173
|
+
function recordClosingUsage(session, turn) {
|
|
137
174
|
const { maxUsd, overRunBudget, estimateTurnCostUsd } = session.settings;
|
|
138
175
|
session.usage.record(turn, "debrief");
|
|
139
176
|
session.lastResponseId = turn.responseId ?? session.lastResponseId;
|
|
@@ -144,13 +181,16 @@ function acceptDebriefTurn(session, turn, trigger, ask, record) {
|
|
|
144
181
|
(maxUsd !== undefined && finalEstimate != null && finalEstimate > maxUsd)) {
|
|
145
182
|
session.trace.record("notice", () => notice("warn", "model budget reached during closing report", "The closing request crossed an estimated budget; no further requests or actions followed. Task completion is unchanged."));
|
|
146
183
|
}
|
|
147
|
-
|
|
184
|
+
return isCompleteTurnUsage(turn.usage) && turn.providerRequest?.usageComplete !== false;
|
|
185
|
+
}
|
|
186
|
+
/** Accept a closing reply as the debrief's result; `note` adds why no later reply replaced it. */
|
|
187
|
+
function acceptDebriefTurn(session, turn, trigger, ask, record, usageReported, note = "") {
|
|
148
188
|
if (turn.actions.length > 0 || turn.pendingSafetyChecks.length > 0) {
|
|
149
|
-
return record("failed",
|
|
189
|
+
return record("failed", `the closing response requested actions or safety checks; none were executed and its report was not accepted${note}`, usageReported);
|
|
150
190
|
}
|
|
151
191
|
// First-party providers parse their replies already; these parses cover third-party ports.
|
|
152
192
|
const { status, detail, ...added } = ask.read(session, turn, trigger);
|
|
153
|
-
return { ...record(status, detail
|
|
193
|
+
return { ...record(status, `${detail}${note}`, usageReported), ...added };
|
|
154
194
|
}
|
|
155
195
|
/**
|
|
156
196
|
* A closing report: its summary and friction as one participant message, then its impressions.
|
|
@@ -205,6 +245,6 @@ function readImpressions(session, turn) {
|
|
|
205
245
|
session.impressions = recordImpressions(session, impressions.data);
|
|
206
246
|
return {
|
|
207
247
|
status: "completed",
|
|
208
|
-
detail: "
|
|
248
|
+
detail: "a read-only impressions request; no additional desktop actions; the participant's own ending is unchanged",
|
|
209
249
|
};
|
|
210
250
|
}
|
|
@@ -203,6 +203,12 @@ export interface CuaTurn {
|
|
|
203
203
|
closingReport?: ParticipantClosingReport;
|
|
204
204
|
/** Present only for an accepted impressions-only closing reply. */
|
|
205
205
|
impressions?: ParticipantImpression[];
|
|
206
|
+
/**
|
|
207
|
+
* Set on a closing reply that was not the JSON its request asked for, when the provider's next
|
|
208
|
+
* closing request would ask for it more strictly. The loop may then call the same closing method
|
|
209
|
+
* once more, after the checks the first request passed and with a time bound of its own.
|
|
210
|
+
*/
|
|
211
|
+
closingRetry?: true;
|
|
206
212
|
}
|
|
207
213
|
/** The model side of the loop. Self-describes its identity and capabilities. */
|
|
208
214
|
export interface CuaProvider {
|
|
@@ -246,7 +252,10 @@ export interface CuaProvider {
|
|
|
246
252
|
/** Latest known usage of the continuing request. Used only for runtime spend guards. */
|
|
247
253
|
readonly pendingRequestUsage?: CuaTurn["usage"];
|
|
248
254
|
nextTurn(req: CuaTurnRequest, signal: AbortSignal, spend?: CuaSpendGate): Promise<CuaTurn>;
|
|
249
|
-
/**
|
|
255
|
+
/**
|
|
256
|
+
* Optional read-only closing report. Implementations must disable tools and send one request per
|
|
257
|
+
* call, with no retries of their own; a second request is the loop's call (`closingRetry`).
|
|
258
|
+
*/
|
|
250
259
|
debrief?: ((req: CuaTurnRequest, signal: AbortSignal) => Promise<CuaTurn>) | undefined;
|
|
251
260
|
/**
|
|
252
261
|
* Optional read-only request for impressions only, after the participant ended the session
|
|
@@ -319,12 +328,12 @@ export interface CuaLoopOptions {
|
|
|
319
328
|
maxWaitMs?: number;
|
|
320
329
|
/**
|
|
321
330
|
* How long a `wait` that names no duration lasts, in ms, when its turn only waits or takes
|
|
322
|
-
* screenshots: the study's actor.idleWaitMs. It is also
|
|
323
|
-
*
|
|
324
|
-
* an action in the same turn such a wait lasts 500 ms. A whole
|
|
325
|
-
* longer than an explicit maxWaitMs; any other value throws a
|
|
326
|
-
* starts. Default 5000. The session never applies more than its
|
|
327
|
-
* the applied lengths as `waitSettings`.
|
|
331
|
+
* screenshots: the study's actor.idleWaitMs. It is also the shortest such a turn is watched for,
|
|
332
|
+
* and how often the loop looks at the screen while it is watched, at most every 30000 ms when the
|
|
333
|
+
* executor is speechEnabled. After an action in the same turn such a wait lasts 500 ms. A whole
|
|
334
|
+
* number from 1000 to 600000 and no longer than an explicit maxWaitMs; any other value throws a
|
|
335
|
+
* RangeError before the session starts. Default 5000. The session never applies more than its
|
|
336
|
+
* maxWaitMs, and the trace records the applied lengths as `waitSettings`.
|
|
328
337
|
*/
|
|
329
338
|
idleWaitMs?: number;
|
|
330
339
|
/**
|
|
@@ -50,10 +50,13 @@ export class IdleWatcher {
|
|
|
50
50
|
if (plannedMs === 0)
|
|
51
51
|
return undefined;
|
|
52
52
|
const capMs = Math.max(plannedMs, session.waits.watchMaxMs);
|
|
53
|
-
// Never shorter than
|
|
53
|
+
// Never shorter than idleWaitMs, so a participant naming a few milliseconds on a static screen
|
|
54
|
+
// does not send requests back to back, and never shorter than the turn's own planned length, so
|
|
55
|
+
// a long named wait is not cut short.
|
|
56
|
+
const shortestMs = Math.max(plannedMs, session.waits.idleWaitMs);
|
|
54
57
|
const limitMs = this.grownFromMs === undefined
|
|
55
|
-
?
|
|
56
|
-
: Math.min(capMs, Math.max(
|
|
58
|
+
? shortestMs
|
|
59
|
+
: Math.min(capMs, Math.max(shortestMs, 2 * this.grownFromMs));
|
|
57
60
|
const ending = await watchScreen(session, this.observer, turnNumber, reference, limitMs);
|
|
58
61
|
// A small change that held still (a status label, or a counter the rule did not hold back)
|
|
59
62
|
// wakes the participant and keeps the doubling, so a repeating small change cannot keep every
|
|
@@ -64,7 +67,7 @@ export class IdleWatcher {
|
|
|
64
67
|
wake: ending.wake,
|
|
65
68
|
...(ending.observation === undefined ? {} : { observation: ending.observation }),
|
|
66
69
|
...(ending.stop === undefined ? {} : { stop: ending.stop }),
|
|
67
|
-
hint: hintFor(ending
|
|
70
|
+
hint: hintFor(ending),
|
|
68
71
|
waits: {
|
|
69
72
|
...splitWatch(lengths, limitMs, ending.ms),
|
|
70
73
|
sessionEnded: ending.stop !== undefined,
|
|
@@ -215,21 +218,18 @@ function splitWatch(lengths, limitMs, ms) {
|
|
|
215
218
|
};
|
|
216
219
|
}
|
|
217
220
|
/**
|
|
218
|
-
* The next request's note. A clock ending says
|
|
221
|
+
* The next request's note. A clock ending says only how long the wait lasted. A
|
|
219
222
|
* small change is named by its place, so a participant is not told the screen changed when what
|
|
220
|
-
* changed was a counter or a timestamp.
|
|
221
|
-
*
|
|
223
|
+
* changed was a counter or a timestamp. The note says nothing of the next wait's length: a
|
|
224
|
+
* participant told it names that length and keeps waiting.
|
|
222
225
|
*/
|
|
223
|
-
function hintFor(ending
|
|
226
|
+
function hintFor(ending) {
|
|
224
227
|
const lasted = `Your wait lasted ${duration(ending.ms)}`;
|
|
225
|
-
const grows = growsToMs === undefined
|
|
226
|
-
? ""
|
|
227
|
-
: ` A wait right after it can last twice as long, at most ${duration(growsToMs)}.`;
|
|
228
228
|
const { seen } = ending;
|
|
229
229
|
switch (ending.wake) {
|
|
230
230
|
case "changed":
|
|
231
231
|
return seen?.verdict === "settled" && seen.small
|
|
232
|
-
? `${lasted} and ended when a small part of the screen changed and then stayed the same for over a minute, ${place(seen.area)}
|
|
232
|
+
? `${lasted} and ended when a small part of the screen changed and then stayed the same for over a minute, ${place(seen.area)}.`
|
|
233
233
|
: `${lasted} and ended when the screen changed.`;
|
|
234
234
|
case "speech":
|
|
235
235
|
return `${lasted} and ended when you heard speech.`;
|
|
@@ -239,13 +239,15 @@ function hintFor(ending, growsToMs) {
|
|
|
239
239
|
// never says the screen stayed the same.
|
|
240
240
|
case "limit":
|
|
241
241
|
if (seen?.verdict === "changing" && seen.lookSoon)
|
|
242
|
-
return `${lasted}; a large part of the screen was changing at its last check
|
|
242
|
+
return `${lasted}; a large part of the screen was changing at its last check.`;
|
|
243
243
|
if (seen?.verdict !== "settled" && seen?.held !== undefined)
|
|
244
|
-
return `${lasted}; a small part of the screen changed and stayed the same, ${place(seen.held)}. A small change does not end a wait
|
|
245
|
-
return `${lasted}; no change on the screen held still long enough to end it
|
|
244
|
+
return `${lasted}; a small part of the screen changed and stayed the same, ${place(seen.held)}. A small change does not end a wait.`;
|
|
245
|
+
return `${lasted}; no change on the screen held still long enough to end it.`;
|
|
246
246
|
case "stalled":
|
|
247
247
|
return `${lasted}; the screen could not be checked during it.`;
|
|
248
|
+
// The clock text that follows says why the wait ended.
|
|
248
249
|
case "clock":
|
|
250
|
+
return `${lasted}.`;
|
|
249
251
|
case "stopped":
|
|
250
252
|
return undefined;
|
|
251
253
|
}
|
|
@@ -4,16 +4,17 @@ import { asArray, asRecord, inputMessage, openAiActionToCua } from "./openai-wir
|
|
|
4
4
|
// provider still threads on previous_response_id feed it too, so a switch partway through a
|
|
5
5
|
// session keeps everything before the switch.
|
|
6
6
|
//
|
|
7
|
-
// The Responses API pairs items: every computer_call needs its computer_call_output,
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// step: the opening screenshot goes first, then
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
7
|
+
// The Responses API pairs items: every computer_call needs its computer_call_output, every
|
|
8
|
+
// function_call (wait_minutes) its function_call_output, and a reasoning item travels with the
|
|
9
|
+
// item it preceded. The conversation is therefore cut only at exchange boundaries: one reply's
|
|
10
|
+
// output items together with the items that answered them. When the estimate passes the budget, the
|
|
11
|
+
// conversation is cut down to half the budget in one step: the opening screenshot goes first, then
|
|
12
|
+
// the oldest exchanges become lines of a progress note that keeps their reasoning summaries,
|
|
13
|
+
// messages, actions and function calls, and the text that answered them, such as a note that an
|
|
14
|
+
// action was not run or how a wait ended. The note is an assistant message, so text the model wrote
|
|
15
|
+
// (which can quote a web page) keeps the trust it had when the model wrote it. The newest
|
|
16
|
+
// MIN_KEPT_EXCHANGES exchanges are never collapsed, so a request passes the budget when they alone
|
|
17
|
+
// exceed it; the trace's per-request estimate shows when that happens.
|
|
17
18
|
//
|
|
18
19
|
// A cut rewrites the start of the prompt, so the provider's prompt cache misses on the request
|
|
19
20
|
// after it. Between cuts nothing already sent changes: each request starts with the whole of the
|
|
@@ -156,12 +157,15 @@ function describeExchange(exchange) {
|
|
|
156
157
|
const said = [];
|
|
157
158
|
const did = [];
|
|
158
159
|
const told = [];
|
|
159
|
-
// The answers are the
|
|
160
|
+
// The answers are the computer_call outputs, whose content is a screenshot, the function call
|
|
161
|
+
// outputs, which say how a wait ended as text beside any screenshot, and hint messages.
|
|
160
162
|
for (const raw of exchange.answers) {
|
|
161
163
|
const item = asRecord(raw);
|
|
162
164
|
if (item.type === "computer_call_output")
|
|
163
165
|
continue;
|
|
164
|
-
|
|
166
|
+
if (typeof item.output === "string" && item.output.length > 0)
|
|
167
|
+
told.push(item.output);
|
|
168
|
+
for (const entry of [...asArray(item.content), ...asArray(item.output)]) {
|
|
165
169
|
const text = asRecord(entry).text;
|
|
166
170
|
if (typeof text === "string" && text.length > 0)
|
|
167
171
|
told.push(text);
|
|
@@ -189,6 +193,11 @@ function describeExchange(exchange) {
|
|
|
189
193
|
did.push(describeAction(mapped));
|
|
190
194
|
}
|
|
191
195
|
}
|
|
196
|
+
// A function call is noted as it was made, such as wait_minutes {"minutes":3}.
|
|
197
|
+
if (item.type === "function_call" && typeof item.name === "string")
|
|
198
|
+
did.push(typeof item.arguments === "string"
|
|
199
|
+
? `${item.name} ${clip(item.arguments, 120)}`
|
|
200
|
+
: item.name);
|
|
192
201
|
}
|
|
193
202
|
const lead = `Turn ${exchange.turn}: `;
|
|
194
203
|
const before = [
|
|
@@ -224,14 +233,17 @@ function clip(text, limit) {
|
|
|
224
233
|
const flat = text.replace(/\s+/g, " ").trim();
|
|
225
234
|
return flat.length <= limit ? flat : `${flat.slice(0, limit - 3)}...`;
|
|
226
235
|
}
|
|
227
|
-
/**
|
|
236
|
+
/**
|
|
237
|
+
* The data URLs of the images an input item carries: a computer_call output's screenshot, and the
|
|
238
|
+
* image parts of a message's content or a function call output's output.
|
|
239
|
+
*/
|
|
228
240
|
function imagesOf(item) {
|
|
229
241
|
const record = asRecord(item);
|
|
230
242
|
const output = asRecord(record.output);
|
|
231
243
|
const urls = [];
|
|
232
244
|
if (typeof output.image_url === "string")
|
|
233
245
|
urls.push(output.image_url);
|
|
234
|
-
for (const part of asArray(record.content)) {
|
|
246
|
+
for (const part of [...asArray(record.content), ...asArray(record.output)]) {
|
|
235
247
|
const url = asRecord(part).image_url;
|
|
236
248
|
if (typeof url === "string")
|
|
237
249
|
urls.push(url);
|
|
@@ -40,6 +40,10 @@ export declare class OpenAiConversation {
|
|
|
40
40
|
private lastResponseId;
|
|
41
41
|
/** computer_call ids the next request owes an output for. */
|
|
42
42
|
private pendingCallIds;
|
|
43
|
+
/** The function calls the next request owes an output for. */
|
|
44
|
+
private pendingFunctionCalls;
|
|
45
|
+
/** Whether the latest reply acted, so its waits went to the desktop and were not watched. */
|
|
46
|
+
private pendingActed;
|
|
43
47
|
/** Accepted replies so far; the next request is number `replies + 1`. */
|
|
44
48
|
private replies;
|
|
45
49
|
/** The whole conversation, which explicit-context requests carry within its token budget. */
|
|
@@ -48,12 +52,22 @@ export declare class OpenAiConversation {
|
|
|
48
52
|
/** Undefined once the account or model rejected reasoning summaries. */
|
|
49
53
|
private reasoningSummary;
|
|
50
54
|
private readonly evidence;
|
|
55
|
+
/** The items of a closing request whose reply was not the stated JSON, for its strict retry. */
|
|
56
|
+
private openClosing;
|
|
51
57
|
constructor(settings: OpenAiConversationSettings);
|
|
52
58
|
/**
|
|
53
|
-
* The next request, carrying `req`'s screen: the opening request, or the
|
|
54
|
-
* reply's computer calls are owed.
|
|
59
|
+
* The next participant turn's request, carrying `req`'s screen: the opening request, or the
|
|
60
|
+
* outputs the latest reply's computer and function calls are owed.
|
|
55
61
|
*/
|
|
56
|
-
request(req: CuaTurnRequest
|
|
62
|
+
request(req: CuaTurnRequest): ConversationRequest;
|
|
63
|
+
/**
|
|
64
|
+
* The read-only closing request carrying `req`'s screen, sent once with `send`. It goes out
|
|
65
|
+
* without text.format, which is part of the cached prefix, so it reads the session from the
|
|
66
|
+
* cache. A completed reply that is not JSON the schema accepts comes back marked closingRetry and
|
|
67
|
+
* leaves the conversation where it was, so the next closing request is the same one with the
|
|
68
|
+
* strict schema set. Returns the reply's turn with the account it gave.
|
|
69
|
+
*/
|
|
70
|
+
close(req: CuaTurnRequest, closing: ClosingRequest, send: (body: Record<string, unknown>) => Promise<unknown>): Promise<CuaTurn>;
|
|
57
71
|
/**
|
|
58
72
|
* The organization rejected server-side state: carry the conversation on the client from the
|
|
59
73
|
* request being sent, and record which answer made the switch. False when the conversation is
|
|
@@ -69,9 +83,20 @@ export declare class OpenAiConversation {
|
|
|
69
83
|
record(): ActorConversation;
|
|
70
84
|
/**
|
|
71
85
|
* The items a request adds to the conversation: the opening message with the first screen, or
|
|
72
|
-
* an output for each pending
|
|
86
|
+
* an output for each pending call with the latest screenshot, and any hint. Each computer_call
|
|
87
|
+
* output carries the screenshot. Function call outputs follow in reply order: with no
|
|
88
|
+
* computer_call the first carries the screenshot, and every other one carries text only, so the
|
|
89
|
+
* screenshot is sent once. A turn's hint goes into the first function call output, so it reads
|
|
90
|
+
* as how the wait ended; a closing request's hint asks for the closing account and stays a user
|
|
91
|
+
* message. A call to a tool that does not exist gets an output that says so (callNote).
|
|
73
92
|
*/
|
|
74
93
|
private newItems;
|
|
94
|
+
/**
|
|
95
|
+
* What a call's output says when no note does, so every reply says how the wait went: a wait
|
|
96
|
+
* among actions ran for its length (a shortened or unsent one has a note), and a further call in
|
|
97
|
+
* a reply of only waits was part of the one watch the first call's output reports.
|
|
98
|
+
*/
|
|
99
|
+
private waitOutcome;
|
|
75
100
|
private context;
|
|
76
101
|
/**
|
|
77
102
|
* A participant request: the opening request, or a continuation carrying `sent`. A threaded
|
|
@@ -79,12 +104,17 @@ export declare class OpenAiConversation {
|
|
|
79
104
|
* ahead of `sent`.
|
|
80
105
|
*/
|
|
81
106
|
private turnBody;
|
|
82
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* A read-only closing request: the next turn's body with tools off and its own output limit,
|
|
109
|
+
* and the strict schema only when `schema` is set.
|
|
110
|
+
*/
|
|
83
111
|
private closingBody;
|
|
84
112
|
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
113
|
+
* A participant turn's reply to a request that added `sent`. A reply cut off by the output
|
|
114
|
+
* limit is set aside, so the next request is the same one again (outputLimitRetry); its actions
|
|
115
|
+
* never run, so no call output is owed for them.
|
|
88
116
|
*/
|
|
89
117
|
private accept;
|
|
118
|
+
/** Move the conversation on to a reply to a request that added `sent`. */
|
|
119
|
+
private moveOn;
|
|
90
120
|
}
|