humanish 0.112.0 → 0.113.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 +19 -0
- package/README.md +11 -0
- package/TELEMETRY.md +21 -0
- package/dist/actors/codex/restricted-participant-policy.d.ts +21 -1
- package/dist/actors/codex/restricted-participant-policy.js +20 -2
- package/dist/actors/codex/restricted-participant.d.ts +1 -1
- package/dist/actors/codex/restricted-participant.js +2 -1
- package/dist/actors/computer-use/loop/debrief.d.ts +12 -4
- package/dist/actors/computer-use/loop/debrief.js +57 -20
- package/dist/actors/computer-use/loop/ending.d.ts +5 -2
- package/dist/actors/computer-use/loop/impressions.d.ts +14 -0
- package/dist/actors/computer-use/loop/impressions.js +40 -0
- package/dist/actors/computer-use/loop/session.d.ts +3 -1
- package/dist/actors/computer-use/loop/session.js +2 -0
- package/dist/actors/computer-use/loop/trace.js +2 -0
- package/dist/actors/computer-use/loop/types.d.ts +8 -1
- package/dist/actors/computer-use/loop.d.ts +2 -1
- package/dist/actors/computer-use/loop.js +15 -3
- package/dist/actors/computer-use/openai-conversation.d.ts +9 -4
- package/dist/actors/computer-use/openai-conversation.js +53 -24
- package/dist/actors/computer-use/openai-provider.js +18 -12
- package/dist/actors/contract.d.ts +43 -3
- package/dist/actors/contract.js +13 -0
- package/dist/analysis/evidence-sources.d.ts +3 -1
- package/dist/analysis/evidence-sources.js +10 -1
- package/dist/analysis/evidence.js +2 -2
- package/dist/analysis/execute.d.ts +1 -1
- package/dist/analysis/execute.js +3 -1
- package/dist/cli/commands/runs.js +33 -10
- package/dist/cli/commands/setup.js +95 -6
- package/dist/cli/commands/study-format.js +7 -4
- package/dist/cli/commands/study.js +1 -1
- package/dist/cli/commands/tui.js +1 -1
- package/dist/cli/doctor.d.ts +2 -0
- package/dist/cli/doctor.js +18 -0
- package/dist/cli/io.d.ts +2 -0
- package/dist/cli/program.js +35 -1
- package/dist/cli/secret-prompt.d.ts +5 -1
- package/dist/cli/secret-prompt.js +6 -2
- package/dist/cli/update-check-worker.d.ts +1 -0
- package/dist/cli/update-check-worker.js +5 -0
- package/dist/cli/update-check.d.ts +46 -0
- package/dist/cli/update-check.js +203 -0
- package/dist/feedback/draft.js +18 -0
- package/dist/keys/key-resolution.js +2 -0
- package/dist/keys/key-status.d.ts +37 -0
- package/dist/keys/key-status.js +68 -0
- package/dist/observer-app.html +4 -4
- package/dist/routes/computer-use/local-vm.js +9 -2
- package/dist/routes/computer-use/participant-desktop.d.ts +6 -0
- package/dist/routes/computer-use/participant-execution.js +9 -1
- package/dist/routes/computer-use/rerun-selection.js +23 -21
- package/dist/routes/computer-use/result.js +3 -2
- package/dist/routes/computer-use/self-report.js +7 -1
- package/dist/routes/computer-use/setup.js +14 -0
- package/dist/routes/computer-use/types.d.ts +4 -1
- package/dist/routes/shared-world/bundle-records.js +8 -7
- package/dist/routes/shared-world/bundle.js +1 -1
- package/dist/run/detail.js +4 -4
- package/dist/run/participant-caption.d.ts +2 -1
- package/dist/run/participant-caption.js +11 -1
- package/dist/run/participant-records.d.ts +3 -1
- package/dist/run/participant-records.js +6 -0
- package/dist/run-study.js +17 -1
- package/dist/study/discover.js +6 -1
- package/dist/study/doctor.d.ts +2 -0
- package/dist/study/doctor.js +45 -7
- package/dist/study/init-templates.js +10 -5
- package/dist/study/plan.js +3 -2
- package/dist/study/preflight-result.d.ts +1 -0
- package/dist/study/preflight-result.js +1 -1
- package/dist/study/preflight.d.ts +2 -0
- package/dist/study/preflight.js +54 -14
- package/dist/study/warnings.d.ts +5 -0
- package/dist/study/warnings.js +79 -2
- package/dist/substrates/local/capacity.d.ts +75 -0
- package/dist/substrates/local/capacity.js +143 -0
- package/dist/substrates/local/firecracker-desktop.d.ts +8 -0
- package/dist/substrates/local/firecracker-desktop.js +34 -3
- package/dist/substrates/local/runtime-host.d.ts +16 -3
- package/dist/substrates/local/runtime-host.js +49 -4
- package/dist/substrates/local/runtime.d.ts +10 -0
- package/dist/substrates/local/runtime.js +44 -5
- package/dist/tui-app.js +58 -58
- package/docs/architecture/actor-contract.md +20 -6
- package/docs/contracts/feedback.md +8 -1
- package/docs/contracts/schemas.md +57 -34
- package/docs/contracts/study-analysis.md +15 -0
- package/package.json +1 -1
- package/skills/humanish/SKILL.md +21 -10
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,25 @@ The Unreleased section holds the full notes for the next version until it is tag
|
|
|
8
8
|
|
|
9
9
|
## Unreleased
|
|
10
10
|
|
|
11
|
+
## 0.113.0: Participant impressions, mission and persona warnings, local desktop capacity, key status, update notice (2026-10-07)
|
|
12
|
+
|
|
13
|
+
humanish 0.113.0 asks computer-use participants what they thought at the end of a session, warns
|
|
14
|
+
when a study scripts its participants, and says how many local desktops fit. An OpenAI or Codex
|
|
15
|
+
participant ends with up to six first-person impressions: what looked unclear, unfinished or
|
|
16
|
+
untrustworthy, what they liked, what they expected and did not find, and where the screen differs
|
|
17
|
+
from how they do the same task in their own work or life. The trace keeps them as `impressions`,
|
|
18
|
+
the Observer shows them under "What they said at the end", and analyses at prompt
|
|
19
|
+
`study-evidence-8` can cite them. `study check` and `run` warn when a mission reads like a list of
|
|
20
|
+
UI steps or a participant's persona has no background, and the starter personas `init` writes now
|
|
21
|
+
lead with a background, which changes their participant prompt. `runtime status` and
|
|
22
|
+
`doctor --study` report how many participant desktops the local runtime holds, and a live local
|
|
23
|
+
study that needs more than a Mac's humanish VM holds is refused before any desktop starts.
|
|
24
|
+
`humanish keys` lists each provider key with its source and never its value, and humanish says at
|
|
25
|
+
most once a day, at a terminal, when a newer version is out. Live shared-world output and older
|
|
26
|
+
terminal and scripted runs use plain participant captions.
|
|
27
|
+
|
|
28
|
+
[Release notes](https://github.com/danielgwilson/humanish/releases/tag/v0.113.0)
|
|
29
|
+
|
|
11
30
|
## 0.112.0: Plain finding headlines, design findings, plain captions, DO_NOT_TRACK for terminal participants (2026-10-07)
|
|
12
31
|
|
|
13
32
|
humanish 0.112.0 writes findings and captions for someone who did not watch the session, and
|
package/README.md
CHANGED
|
@@ -138,6 +138,8 @@ npx humanish observe --run latest --open
|
|
|
138
138
|
```
|
|
139
139
|
|
|
140
140
|
Existing `E2B_API_KEY` and `OPENAI_API_KEY` environment variables also work.
|
|
141
|
+
`npx humanish keys` shows which keys are set and where each comes from, and
|
|
142
|
+
`npx humanish keys set` with no vendor asks for each missing key in turn.
|
|
141
143
|
`doctor --study` checks the study's local setup without launching a
|
|
142
144
|
desktop or making a model request. It reports key presence, not remote key
|
|
143
145
|
validity, model access, or quota. `study check` checks manifest metadata by default.
|
|
@@ -364,6 +366,7 @@ has the providers' retention terms.
|
|
|
364
366
|
| Codex or Claude Code | The same screenshots and prompts, through your signed-in agent and its provider | `local-agent` participants, local browser studies, the Codex analyst |
|
|
365
367
|
| E2B | A desktop or shell running your app and everything on its screen; the repository a `clone` subject names; your working tree for a `local-tree` subject, minus gitignored files, `.env*` and other secret-shaped files; the credentials you declare for the subject app; for a terminal study with `execution.runtimeAuth: openai-env`, your OpenAI key | Every hosted study |
|
|
366
368
|
| PostHog | Telemetry events, described [below](#check-what-telemetry-sends) | By default |
|
|
369
|
+
| npm registry | A request for the latest humanish version, with no identifying data, described [below](#check-what-telemetry-sends) | At most once a day, at a terminal |
|
|
367
370
|
|
|
368
371
|
## Find the main commands
|
|
369
372
|
|
|
@@ -476,6 +479,14 @@ subjects, personas, prompts, paths, run ids and evidence are never sent.
|
|
|
476
479
|
disable` or `DO_NOT_TRACK=1` turns it off. [TELEMETRY.md](TELEMETRY.md) lists
|
|
477
480
|
every field.
|
|
478
481
|
|
|
482
|
+
At most once a day, a command run at a terminal also asks the npm registry for
|
|
483
|
+
the latest humanish version, and a later command prints one line on stderr when
|
|
484
|
+
yours is older. That request is a plain `GET` of humanish's dist-tags with no
|
|
485
|
+
machine id, version, command or path. It is skipped in CI, without a terminal,
|
|
486
|
+
for `--json`, and with `DO_NOT_TRACK=1`, `HUMANISH_TELEMETRY_DISABLED=1` or
|
|
487
|
+
`HUMANISH_NO_UPDATE_CHECK=1`. [TELEMETRY.md](TELEMETRY.md#the-update-check)
|
|
488
|
+
describes it.
|
|
489
|
+
|
|
479
490
|
## Contribute
|
|
480
491
|
|
|
481
492
|
Contributors: read
|
package/TELEMETRY.md
CHANGED
|
@@ -112,3 +112,24 @@ humanish-specific configuration. The opt-out is stored under your user config
|
|
|
112
112
|
|
|
113
113
|
Telemetry never blocks, slows, or fails a command: it is fire-and-forget, bounded
|
|
114
114
|
by a two-second timeout, and every error inside it is swallowed.
|
|
115
|
+
|
|
116
|
+
## The update check
|
|
117
|
+
|
|
118
|
+
A command run at a terminal also checks, at most once a day, whether a newer
|
|
119
|
+
humanish is published. This is a separate request and sends no event. A
|
|
120
|
+
background process asks the npm registry for
|
|
121
|
+
`https://registry.npmjs.org/-/package/humanish/dist-tags` with a plain `GET`
|
|
122
|
+
whose only header is `Accept`. It carries no machine id, installed version,
|
|
123
|
+
command or path; the registry sees the request's source address, as it does for
|
|
124
|
+
any `npm install`. The answer is cached next to the telemetry state, in
|
|
125
|
+
`~/.config/humanish/update-check.json`, and a later command prints one line on
|
|
126
|
+
stderr when your version is older. The check never delays or fails a command.
|
|
127
|
+
|
|
128
|
+
It is skipped without a terminal, in CI, for `--json` output, for a study
|
|
129
|
+
participant, and when any of these is set:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
DO_NOT_TRACK=1
|
|
133
|
+
HUMANISH_TELEMETRY_DISABLED=1
|
|
134
|
+
HUMANISH_NO_UPDATE_CHECK=1
|
|
135
|
+
```
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type CuaTurn } from "../computer-use/loop.js";
|
|
2
|
-
import type
|
|
2
|
+
import { type ActorExecutionProfile, type ParticipantClosingReport } from "../contract.js";
|
|
3
3
|
/** The declared profile. A participant replaces cliVersion with its detected CLI release, or with
|
|
4
4
|
* this host's newest qualified release before its first launch. */
|
|
5
5
|
export declare const PARTICIPANT_PROFILE: Readonly<ActorExecutionProfile>;
|
|
@@ -39,6 +39,26 @@ export declare const PARTICIPANT_FINAL_SCHEMA: {
|
|
|
39
39
|
maxLength: number;
|
|
40
40
|
};
|
|
41
41
|
};
|
|
42
|
+
impressions: {
|
|
43
|
+
type: string;
|
|
44
|
+
maxItems: number;
|
|
45
|
+
items: {
|
|
46
|
+
type: string;
|
|
47
|
+
additionalProperties: boolean;
|
|
48
|
+
required: string[];
|
|
49
|
+
properties: {
|
|
50
|
+
kind: {
|
|
51
|
+
type: string;
|
|
52
|
+
enum: ("liked" | "missing" | "unclear" | "unfinished" | "unlike_my_work" | "untrustworthy")[];
|
|
53
|
+
};
|
|
54
|
+
text: {
|
|
55
|
+
type: string;
|
|
56
|
+
minLength: number;
|
|
57
|
+
maxLength: number;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
};
|
|
42
62
|
};
|
|
43
63
|
};
|
|
44
64
|
/** humanish tool arguments, validated before the shared executor sees a batch. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { BROWSER_CONTROL_LIMITS, browserControlActionSchema, browserOnlyControlActionSchema, validateBrowserControlAction, } from "../../browser-control/protocol.js";
|
|
3
3
|
import { validClosingReport } from "../computer-use/loop.js";
|
|
4
|
+
import { PARTICIPANT_IMPRESSION_KINDS, } from "../contract.js";
|
|
4
5
|
/** The declared profile. A participant replaces cliVersion with its detected CLI release, or with
|
|
5
6
|
* this host's newest qualified release before its first launch. */
|
|
6
7
|
export const PARTICIPANT_PROFILE = Object.freeze({
|
|
@@ -40,7 +41,7 @@ export const PARTICIPANT_TOOL_SCHEMA = participantToolSchema();
|
|
|
40
41
|
export const PARTICIPANT_FINAL_SCHEMA = {
|
|
41
42
|
type: "object",
|
|
42
43
|
additionalProperties: false,
|
|
43
|
-
required: ["outcome", "summary", "frictionReports"],
|
|
44
|
+
required: ["outcome", "summary", "frictionReports", "impressions"],
|
|
44
45
|
properties: {
|
|
45
46
|
outcome: { type: "string", enum: ["reached", "not_reached", "blocked"] },
|
|
46
47
|
summary: { type: "string", minLength: 1, maxLength: 4000 },
|
|
@@ -49,6 +50,19 @@ export const PARTICIPANT_FINAL_SCHEMA = {
|
|
|
49
50
|
maxItems: 8,
|
|
50
51
|
items: { type: "string", minLength: 1, maxLength: 2000 },
|
|
51
52
|
},
|
|
53
|
+
impressions: {
|
|
54
|
+
type: "array",
|
|
55
|
+
maxItems: 6,
|
|
56
|
+
items: {
|
|
57
|
+
type: "object",
|
|
58
|
+
additionalProperties: false,
|
|
59
|
+
required: ["kind", "text"],
|
|
60
|
+
properties: {
|
|
61
|
+
kind: { type: "string", enum: [...PARTICIPANT_IMPRESSION_KINDS] },
|
|
62
|
+
text: { type: "string", minLength: 1, maxLength: 500 },
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
52
66
|
},
|
|
53
67
|
};
|
|
54
68
|
/**
|
|
@@ -105,6 +119,10 @@ export function parseParticipantFinal(value) {
|
|
|
105
119
|
done: true,
|
|
106
120
|
outcome,
|
|
107
121
|
message: [report.summary, ...report.frictionReports].join("\n"),
|
|
108
|
-
closingReport: {
|
|
122
|
+
closingReport: {
|
|
123
|
+
summary: report.summary,
|
|
124
|
+
frictionReports: [...report.frictionReports],
|
|
125
|
+
...(report.impressions === undefined ? {} : { impressions: [...report.impressions] }),
|
|
126
|
+
},
|
|
109
127
|
};
|
|
110
128
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type CuaProvider } from "../computer-use/loop.js";
|
|
2
2
|
import { type RestrictedCodexSessionOptions } from "./restricted-session.js";
|
|
3
3
|
import type { RestrictedCodexAnalysisErrorCode } from "./restricted-policy.js";
|
|
4
4
|
import type { ReasoningEffort } from "../reasoning-effort.js";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {} from "../contract.js";
|
|
2
2
|
import { defaultCodexCliVersion, untestedOperatorReleaseWarning } from "./codex-admission.js";
|
|
3
|
+
import { IMPRESSIONS_ASK, } from "../computer-use/loop.js";
|
|
3
4
|
import { ComputerUseProviderError, isComputerUseProviderError, } from "../computer-use/provider-error.js";
|
|
4
5
|
import { validateBrowserControlPng, validateHeardSpeech } from "../../browser-control/protocol.js";
|
|
5
6
|
import { createRestrictedCodexSession, } from "./restricted-session.js";
|
|
@@ -40,7 +41,7 @@ function deferred() {
|
|
|
40
41
|
}
|
|
41
42
|
// prose-check: model prompt (Codex reads this tool description, not a person)
|
|
42
43
|
const toolDescription = (speechEnabled) => `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 wait action lasts at most ${L.waitMs} ms; a longer wait is shortened to ${L.waitMs} ms, so wait longer with another call.${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.`;
|
|
43
|
-
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 and
|
|
44
|
+
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.`;
|
|
44
45
|
/** Turns and failures from the native run, handed in order to the next provider request. */
|
|
45
46
|
class ParticipantEvents {
|
|
46
47
|
queued = [];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ActorTrace, type ParticipantClosingReport, type ParticipantImpression } from "../../contract.js";
|
|
2
2
|
import type { DebriefTrigger } from "./ending.js";
|
|
3
3
|
import type { LoopSession } from "./session.js";
|
|
4
4
|
import type { CuaTurnRequest } from "./types.js";
|
|
@@ -9,11 +9,19 @@ export interface DebriefContext {
|
|
|
9
9
|
readonly previousExecution: CuaTurnRequest["previousExecution"];
|
|
10
10
|
readonly acknowledgedSafetyChecks: CuaTurnRequest["acknowledgedSafetyChecks"];
|
|
11
11
|
}
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Runtime validation is also required for third-party provider ports. A report without
|
|
14
|
+
* impressions stays valid: older providers and stored traces do not have them.
|
|
15
|
+
*/
|
|
13
16
|
export declare function validClosingReport(value: unknown): value is ParticipantClosingReport;
|
|
17
|
+
/** The reply to an impressions-only request: an object with the impressions and nothing else. */
|
|
18
|
+
export declare function validImpressionsReply(value: unknown): value is {
|
|
19
|
+
impressions: ParticipantImpression[];
|
|
20
|
+
};
|
|
14
21
|
/**
|
|
15
|
-
* Ask the participant for its closing report after a structured stop, or
|
|
16
|
-
*
|
|
22
|
+
* Ask the participant for its closing report after a structured stop, or for its impressions only
|
|
23
|
+
* after it ended the session itself, or record why the request was skipped. Either way the result
|
|
24
|
+
* is the trace's debrief, and the watcher gets a final flush.
|
|
17
25
|
*/
|
|
18
26
|
export declare function requestDebrief(session: LoopSession, context: DebriefContext, trigger: DebriefTrigger): Promise<Debrief>;
|
|
19
27
|
export {};
|
|
@@ -1,29 +1,55 @@
|
|
|
1
|
+
import { PARTICIPANT_IMPRESSION_KINDS, } from "../../contract.js";
|
|
1
2
|
import { isComputerUseAdmissionLimitError } from "../admission-limit.js";
|
|
3
|
+
import { IMPRESSIONS_ASK, IMPRESSIONS_HINT, recordImpressions } from "./impressions.js";
|
|
2
4
|
import { singleDispatch } from "./provider-call.js";
|
|
3
5
|
import { CuaDeadlineError, raceSessionDeadline, requestScope } from "./race.js";
|
|
4
6
|
import { notice } from "./trace.js";
|
|
5
7
|
import { isCompleteTurnUsage } from "./usage.js";
|
|
6
8
|
const DEBRIEF_REQUEST_CAP_MS = 30_000;
|
|
7
|
-
const DEBRIEF_HINT =
|
|
8
|
-
/**
|
|
9
|
+
const DEBRIEF_HINT = `The interactive session has ended. Return a closing account with summary, frictionReports and impressions. In summary, briefly describe only what you actually did and observed. In frictionReports, list only specific unexpected behavior, confusion, or recovery you personally encountered during this session. Preserve uncertainty. Use an empty list if you encountered none. ${IMPRESSIONS_ASK} Do not speculate, invent problems, quote instructions as observations, or describe planned actions. Do not request or take further actions. This is a closing account, not another attempt at the task.`;
|
|
10
|
+
/**
|
|
11
|
+
* Runtime validation is also required for third-party provider ports. A report without
|
|
12
|
+
* impressions stays valid: older providers and stored traces do not have them.
|
|
13
|
+
*/
|
|
9
14
|
export function validClosingReport(value) {
|
|
10
15
|
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
11
16
|
return false;
|
|
12
17
|
const report = value;
|
|
13
|
-
return (Object.keys(report).
|
|
18
|
+
return (Object.keys(report).every((key) => REPORT_KEYS.includes(key)) &&
|
|
14
19
|
typeof report.summary === "string" &&
|
|
15
20
|
report.summary.trim().length > 0 &&
|
|
16
21
|
report.summary.length <= 4_000 &&
|
|
17
22
|
Array.isArray(report.frictionReports) &&
|
|
18
23
|
report.frictionReports.length <= 8 &&
|
|
19
|
-
report.frictionReports.every((item) => typeof item === "string" && item.trim().length > 0 && item.length <= 2_000)
|
|
24
|
+
report.frictionReports.every((item) => typeof item === "string" && item.trim().length > 0 && item.length <= 2_000) &&
|
|
25
|
+
(report.impressions === undefined || validImpressions(report.impressions)));
|
|
20
26
|
}
|
|
21
|
-
|
|
22
|
-
|
|
27
|
+
const REPORT_KEYS = ["summary", "frictionReports", "impressions"];
|
|
28
|
+
/** The reply to an impressions-only request: an object with the impressions and nothing else. */
|
|
29
|
+
export function validImpressionsReply(value) {
|
|
30
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
31
|
+
return false;
|
|
32
|
+
const reply = value;
|
|
33
|
+
return Object.keys(reply).length === 1 && validImpressions(reply.impressions);
|
|
34
|
+
}
|
|
35
|
+
function validImpressions(value) {
|
|
36
|
+
return (Array.isArray(value) &&
|
|
37
|
+
value.length <= 6 &&
|
|
38
|
+
value.every((item) => {
|
|
39
|
+
if (item === null || typeof item !== "object" || Array.isArray(item))
|
|
40
|
+
return false;
|
|
41
|
+
const { kind, text, ...rest } = item;
|
|
42
|
+
return (Object.keys(rest).length === 0 &&
|
|
43
|
+
PARTICIPANT_IMPRESSION_KINDS.some((known) => known === kind) &&
|
|
44
|
+
typeof text === "string" &&
|
|
45
|
+
text.trim().length > 0 &&
|
|
46
|
+
text.length <= 500);
|
|
47
|
+
}));
|
|
23
48
|
}
|
|
24
49
|
/**
|
|
25
|
-
* Ask the participant for its closing report after a structured stop, or
|
|
26
|
-
*
|
|
50
|
+
* Ask the participant for its closing report after a structured stop, or for its impressions only
|
|
51
|
+
* after it ended the session itself, or record why the request was skipped. Either way the result
|
|
52
|
+
* is the trace's debrief, and the watcher gets a final flush.
|
|
27
53
|
*/
|
|
28
54
|
export async function requestDebrief(session, context, trigger) {
|
|
29
55
|
const record = (status, detail, usageReported) => {
|
|
@@ -33,13 +59,13 @@ export async function requestDebrief(session, context, trigger) {
|
|
|
33
59
|
reason: session.redactNarration(detail),
|
|
34
60
|
...(usageReported === undefined ? {} : { usageReported }),
|
|
35
61
|
};
|
|
36
|
-
session.trace.record("notice", () => notice(status === "completed" ? "ok" : "warn", `participant debrief ${status}`, session.redactNarration(detail)));
|
|
62
|
+
session.trace.record("notice", () => notice(status === "completed" ? "ok" : "warn", `participant ${trigger.kind === "participant_end" ? "impressions request" : "debrief"} ${status}`, session.redactNarration(detail)));
|
|
37
63
|
return debrief;
|
|
38
64
|
};
|
|
39
65
|
const plan = planDebrief(session, trigger);
|
|
40
66
|
const debrief = "skip" in plan
|
|
41
67
|
? record("skipped", plan.skip)
|
|
42
|
-
: await debriefExchange(session, plan.
|
|
68
|
+
: await debriefExchange(session, plan.ask, context, trigger, plan.boundMs, record);
|
|
43
69
|
session.flush();
|
|
44
70
|
return debrief;
|
|
45
71
|
}
|
|
@@ -48,7 +74,8 @@ function planDebrief(session, trigger) {
|
|
|
48
74
|
const { maxUsd, overRunBudget, estimateTurnCostUsd } = session.settings;
|
|
49
75
|
if (session.trace.counts.turns === 0)
|
|
50
76
|
return { skip: "the study stopped before any participant turn" };
|
|
51
|
-
|
|
77
|
+
const ask = trigger.kind === "participant_end" ? provider.requestImpressions : provider.debrief;
|
|
78
|
+
if (ask === undefined)
|
|
52
79
|
return { skip: "this provider does not support read-only closing reports" };
|
|
53
80
|
if (usage.cleanupUnconfirmed)
|
|
54
81
|
return { skip: "participant request cleanup is unconfirmed" };
|
|
@@ -80,10 +107,10 @@ function planDebrief(session, trigger) {
|
|
|
80
107
|
skip: signal?.aborted ? "the study was cancelled" : "the session deadline was reached",
|
|
81
108
|
};
|
|
82
109
|
}
|
|
83
|
-
return {
|
|
110
|
+
return { ask, boundMs };
|
|
84
111
|
}
|
|
85
|
-
async function debriefExchange(session,
|
|
86
|
-
const { signal } = session;
|
|
112
|
+
async function debriefExchange(session, ask, context, trigger, boundMs, record) {
|
|
113
|
+
const { signal, provider } = session;
|
|
87
114
|
const scope = requestScope(signal);
|
|
88
115
|
const timer = setTimeout(() => scope.end(), boundMs);
|
|
89
116
|
timer.unref?.();
|
|
@@ -101,12 +128,12 @@ async function debriefExchange(session, provider, context, trigger, boundMs, rec
|
|
|
101
128
|
...(context.acknowledgedSafetyChecks === undefined
|
|
102
129
|
? {}
|
|
103
130
|
: { acknowledgedSafetyChecks: context.acknowledgedSafetyChecks }),
|
|
104
|
-
contextHint: DEBRIEF_HINT,
|
|
131
|
+
contextHint: trigger.kind === "participant_end" ? IMPRESSIONS_HINT : DEBRIEF_HINT,
|
|
105
132
|
};
|
|
106
133
|
const turn = provider.requestPolicy === "fail_closed"
|
|
107
|
-
? await singleDispatch(session, "debrief", (dispatchSignal) =>
|
|
108
|
-
: await raceSessionDeadline(
|
|
109
|
-
return acceptDebriefTurn(session, turn, record);
|
|
134
|
+
? await singleDispatch(session, "debrief", (dispatchSignal) => ask.call(provider, request, dispatchSignal), boundMs)
|
|
135
|
+
: await raceSessionDeadline(ask.call(provider, request, scope.signal), boundMs, signal);
|
|
136
|
+
return acceptDebriefTurn(session, turn, trigger, record);
|
|
110
137
|
}
|
|
111
138
|
catch (error) {
|
|
112
139
|
// A failed optional report cannot rewrite the already observed structured completion.
|
|
@@ -116,7 +143,7 @@ async function debriefExchange(session, provider, context, trigger, boundMs, rec
|
|
|
116
143
|
const detail = signal?.aborted
|
|
117
144
|
? "cancelled"
|
|
118
145
|
: scope.signal.aborted || error instanceof CuaDeadlineError
|
|
119
|
-
? "closing report deadline reached
|
|
146
|
+
? `${trigger.kind === "participant_end" ? "impressions request" : "closing report"} deadline reached`
|
|
120
147
|
: error instanceof Error
|
|
121
148
|
? error.message
|
|
122
149
|
: String(error);
|
|
@@ -129,7 +156,7 @@ async function debriefExchange(session, provider, context, trigger, boundMs, rec
|
|
|
129
156
|
scope.end();
|
|
130
157
|
}
|
|
131
158
|
}
|
|
132
|
-
function acceptDebriefTurn(session, turn, record) {
|
|
159
|
+
function acceptDebriefTurn(session, turn, trigger, record) {
|
|
133
160
|
const { maxUsd, overRunBudget, estimateTurnCostUsd } = session.settings;
|
|
134
161
|
session.usage.record(turn, "debrief");
|
|
135
162
|
session.lastResponseId = turn.responseId ?? session.lastResponseId;
|
|
@@ -144,6 +171,15 @@ function acceptDebriefTurn(session, turn, record) {
|
|
|
144
171
|
if (turn.actions.length > 0 || turn.pendingSafetyChecks.length > 0) {
|
|
145
172
|
return record("failed", "the closing response requested actions or safety checks; none were executed and its report was not accepted", usageReported);
|
|
146
173
|
}
|
|
174
|
+
if (trigger.kind === "participant_end") {
|
|
175
|
+
if (!validImpressions(turn.impressions)) {
|
|
176
|
+
return record("failed", turn.interruption === "output_limit"
|
|
177
|
+
? "the reply was cut off by the output limit"
|
|
178
|
+
: "the reply did not contain valid impressions", usageReported);
|
|
179
|
+
}
|
|
180
|
+
session.impressions = recordImpressions(session, turn.impressions);
|
|
181
|
+
return record("completed", "one read-only impressions request; no additional desktop actions; the participant's own ending is unchanged", usageReported);
|
|
182
|
+
}
|
|
147
183
|
if (!validClosingReport(turn.closingReport)) {
|
|
148
184
|
return record("failed", "the closing response did not contain a valid structured participant report", usageReported);
|
|
149
185
|
}
|
|
@@ -159,6 +195,7 @@ function acceptDebriefTurn(session, turn, record) {
|
|
|
159
195
|
text: [report.summary, ...report.frictionReports].join("\n\n"),
|
|
160
196
|
}));
|
|
161
197
|
session.trace.bump("messages");
|
|
198
|
+
session.impressions = recordImpressions(session, turn.closingReport.impressions);
|
|
162
199
|
const debrief = record("completed", "one read-only report; no additional desktop actions; original stop and task outcomes preserved", usageReported);
|
|
163
200
|
return { ...debrief, report, messageId };
|
|
164
201
|
}
|
|
@@ -3,9 +3,12 @@ import type { StopConditionMatch } from "../../stop-conditions.js";
|
|
|
3
3
|
import type { LoopSession } from "./session.js";
|
|
4
4
|
import { type Evidence } from "./trace.js";
|
|
5
5
|
import type { CuaObservation, CuaProvider, CuaTurn } from "./types.js";
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* An ending after which the participant is asked for a debrief: a harness-observed completion, or
|
|
8
|
+
* the participant's own ending when only its impressions are missing.
|
|
9
|
+
*/
|
|
7
10
|
export interface DebriefTrigger {
|
|
8
|
-
readonly kind: "stop_when" | "dwell";
|
|
11
|
+
readonly kind: "stop_when" | "dwell" | "participant_end";
|
|
9
12
|
/** The final observation, already captured; the debrief request sends it without observing. */
|
|
10
13
|
readonly observation: CuaObservation;
|
|
11
14
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ActorTrace, ParticipantImpression, ParticipantImpressions } from "../../contract.js";
|
|
2
|
+
import type { LoopSession } from "./session.js";
|
|
3
|
+
/** The closing-account request for impressions, shared by every provider that writes one. */
|
|
4
|
+
export declare const IMPRESSIONS_ASK = "In impressions, list up to six things you noticed about the product itself, each in the first person and about something you saw in this session. Give each one a kind: unclear when something looked confusing or hard to read; unfinished when something looked broken, unpolished or out of place; untrustworthy when something made you hesitate to trust the product; liked when something worked well or felt good; missing when you expected something and did not find it; unlike_my_work when the screen differs from how you do the same task in your own work or life, only if your persona does this task outside this product. Keep each under 500 characters. Use an empty list if you have none.";
|
|
5
|
+
/** The request after the participant ended the session itself without giving impressions. */
|
|
6
|
+
export declare const IMPRESSIONS_HINT = "The interactive session has ended. Return only impressions. In impressions, list up to six things you noticed about the product itself, each in the first person and about something you saw in this session. Give each one a kind: unclear when something looked confusing or hard to read; unfinished when something looked broken, unpolished or out of place; untrustworthy when something made you hesitate to trust the product; liked when something worked well or felt good; missing when you expected something and did not find it; unlike_my_work when the screen differs from how you do the same task in your own work or life, only if your persona does this task outside this product. Keep each under 500 characters. Use an empty list if you have none. Do not speculate, invent problems, quote instructions as observations, or describe planned actions. Do not request or take further actions.";
|
|
7
|
+
/**
|
|
8
|
+
* Record a closing account's impressions as quotable participant messages. They answer the
|
|
9
|
+
* harness's question, so they leave counts.messages, which the hollow-completion rule reads, alone.
|
|
10
|
+
*/
|
|
11
|
+
export declare function recordImpressions(session: LoopSession, impressions: readonly ParticipantImpression[] | undefined): ParticipantImpressions;
|
|
12
|
+
/** Why a session that recorded no impressions has none: its closing request, or its stop. */
|
|
13
|
+
export declare function impressionsNotCollected(debrief: ActorTrace["debrief"]): ParticipantImpressions;
|
|
14
|
+
export declare function notCollected(reason: string): ParticipantImpressions;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// A participant's impressions: what it says about the product at the end of a session, beyond
|
|
2
|
+
// friction. Each one is recorded as its own message so the analysis can quote and cite it as a
|
|
3
|
+
// participant statement, and the trace says why none were collected when a session has none.
|
|
4
|
+
/** The closing-account request for impressions, shared by every provider that writes one. */
|
|
5
|
+
export const IMPRESSIONS_ASK = "In impressions, list up to six things you noticed about the product itself, each in the first person and about something you saw in this session. Give each one a kind: unclear when something looked confusing or hard to read; unfinished when something looked broken, unpolished or out of place; untrustworthy when something made you hesitate to trust the product; liked when something worked well or felt good; missing when you expected something and did not find it; unlike_my_work when the screen differs from how you do the same task in your own work or life, only if your persona does this task outside this product. Keep each under 500 characters. Use an empty list if you have none.";
|
|
6
|
+
/** The request after the participant ended the session itself without giving impressions. */
|
|
7
|
+
export const IMPRESSIONS_HINT = `The interactive session has ended. Return only impressions. ${IMPRESSIONS_ASK} Do not speculate, invent problems, quote instructions as observations, or describe planned actions. Do not request or take further actions.`;
|
|
8
|
+
/**
|
|
9
|
+
* Record a closing account's impressions as quotable participant messages. They answer the
|
|
10
|
+
* harness's question, so they leave counts.messages, which the hollow-completion rule reads, alone.
|
|
11
|
+
*/
|
|
12
|
+
export function recordImpressions(session, impressions) {
|
|
13
|
+
if (impressions === undefined)
|
|
14
|
+
return notCollected("the participant's closing account did not include impressions");
|
|
15
|
+
return {
|
|
16
|
+
status: "collected",
|
|
17
|
+
items: impressions.map(({ kind, text }) => {
|
|
18
|
+
const redacted = session.redactNarration(text.trim());
|
|
19
|
+
const messageId = session.trace.record("message", () => ({
|
|
20
|
+
lifecycle: "completed",
|
|
21
|
+
title: "participant impression",
|
|
22
|
+
text: `Impression (${kind.replaceAll("_", " ")}): ${redacted}`,
|
|
23
|
+
}));
|
|
24
|
+
return { kind, text: redacted, messageId };
|
|
25
|
+
}),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** Why a session that recorded no impressions has none: its closing request, or its stop. */
|
|
29
|
+
export function impressionsNotCollected(debrief) {
|
|
30
|
+
if (debrief === undefined)
|
|
31
|
+
return notCollected("the session stopped before a closing account");
|
|
32
|
+
if (debrief.status === "completed")
|
|
33
|
+
return notCollected("the participant's closing account did not include impressions");
|
|
34
|
+
const request = debrief.trigger === "participant_end" ? "impressions request" : "closing report";
|
|
35
|
+
const outcome = debrief.status === "skipped" ? "was skipped" : "failed";
|
|
36
|
+
return notCollected(`the ${request} ${outcome}: ${debrief.reason}`);
|
|
37
|
+
}
|
|
38
|
+
export function notCollected(reason) {
|
|
39
|
+
return { status: "not_collected", reason };
|
|
40
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ActorTraceItem, ParticipantDeclaredOutcome } from "../../contract.js";
|
|
1
|
+
import type { ActorTraceItem, ParticipantDeclaredOutcome, ParticipantImpressions } from "../../contract.js";
|
|
2
2
|
import type { AffordanceObservation } from "../../affordance.js";
|
|
3
3
|
import { TaskTracker } from "../../../study/tasks.js";
|
|
4
4
|
import type { DebriefTrigger, Stop } from "./ending.js";
|
|
@@ -61,6 +61,8 @@ export declare class LoopSession {
|
|
|
61
61
|
declaredOutcome: ParticipantDeclaredOutcome | undefined;
|
|
62
62
|
/** Set by a structured stop; the debrief follows it even if the session then failed. */
|
|
63
63
|
debriefTrigger: DebriefTrigger | undefined;
|
|
64
|
+
/** Set by the participant's own final account or by its closing report. */
|
|
65
|
+
impressions: ParticipantImpressions | undefined;
|
|
64
66
|
private stopCause;
|
|
65
67
|
private readonly scrubText;
|
|
66
68
|
constructor(options: LoopRunOptions);
|
|
@@ -61,6 +61,8 @@ export class LoopSession {
|
|
|
61
61
|
declaredOutcome;
|
|
62
62
|
/** Set by a structured stop; the debrief follows it even if the session then failed. */
|
|
63
63
|
debriefTrigger;
|
|
64
|
+
/** Set by the participant's own final account or by its closing report. */
|
|
65
|
+
impressions;
|
|
64
66
|
stopCause;
|
|
65
67
|
scrubText;
|
|
66
68
|
constructor(options) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { summarizeAffordanceUse } from "../../affordance.js";
|
|
2
2
|
import { ACTOR_TRACE_SCHEMA, } from "../../contract.js";
|
|
3
|
+
import { impressionsNotCollected } from "./impressions.js";
|
|
3
4
|
import { plural } from "../../../run/text.js";
|
|
4
5
|
export function notice(status, title, text) {
|
|
5
6
|
return { lifecycle: "completed", status, title, text };
|
|
@@ -129,6 +130,7 @@ export function loopResult(session, stop, debrief) {
|
|
|
129
130
|
: {}),
|
|
130
131
|
...(session.declaredOutcome === undefined ? {} : { declaredOutcome: session.declaredOutcome }),
|
|
131
132
|
...(debrief === undefined ? {} : { debrief }),
|
|
133
|
+
impressions: session.impressions ?? impressionsNotCollected(debrief),
|
|
132
134
|
...(usage.interactionUsageIncomplete(session.capDeclared)
|
|
133
135
|
? { interactionUsageIncomplete: true }
|
|
134
136
|
: {}),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { HeardSpeech } from "../speech.js";
|
|
2
|
-
import type { ActorCapabilities, ActorConversation, ActorExecutionProfile, ActorPersonaRef, ActorSessionResult, ActorTokenUsage, ActorTrace, ActorTraceItem, ParticipantClosingReport, ParticipantDeclaredOutcome, ProviderRequestReceipt } from "../../contract.js";
|
|
2
|
+
import type { ActorCapabilities, ActorConversation, ActorExecutionProfile, ActorPersonaRef, ActorSessionResult, ActorTokenUsage, ActorTrace, ActorTraceItem, ParticipantClosingReport, ParticipantDeclaredOutcome, ParticipantImpression, ProviderRequestReceipt } from "../../contract.js";
|
|
3
3
|
import type { RedactionHooks } from "../../../evidence/redaction.js";
|
|
4
4
|
import type { DwellWindow, StopWhen } from "../../stop-conditions.js";
|
|
5
5
|
import type { StudyTask } from "../../../study/tasks.js";
|
|
@@ -200,6 +200,8 @@ export interface CuaTurn {
|
|
|
200
200
|
outcome?: ParticipantDeclaredOutcome;
|
|
201
201
|
/** Present only for an accepted structured closing account. */
|
|
202
202
|
closingReport?: ParticipantClosingReport;
|
|
203
|
+
/** Present only for an accepted impressions-only closing reply. */
|
|
204
|
+
impressions?: ParticipantImpression[];
|
|
203
205
|
}
|
|
204
206
|
/** The model side of the loop. Self-describes its identity and capabilities. */
|
|
205
207
|
export interface CuaProvider {
|
|
@@ -245,6 +247,11 @@ export interface CuaProvider {
|
|
|
245
247
|
nextTurn(req: CuaTurnRequest, signal: AbortSignal, spend?: CuaSpendGate): Promise<CuaTurn>;
|
|
246
248
|
/** Optional read-only closing report. Implementations must disable tools and make no retries. */
|
|
247
249
|
debrief?: ((req: CuaTurnRequest, signal: AbortSignal) => Promise<CuaTurn>) | undefined;
|
|
250
|
+
/**
|
|
251
|
+
* Optional read-only request for impressions only, after the participant ended the session
|
|
252
|
+
* itself without them. Its turn carries `impressions`. Same rules as `debrief`.
|
|
253
|
+
*/
|
|
254
|
+
requestImpressions?: ((req: CuaTurnRequest, signal: AbortSignal) => Promise<CuaTurn>) | undefined;
|
|
248
255
|
/** Release the participant's model resources. Idempotent; reject if cleanup is unconfirmed. */
|
|
249
256
|
close?(): Promise<void>;
|
|
250
257
|
}
|
|
@@ -2,7 +2,8 @@ import type { CuaLoopOptions, CuaLoopResult, LoopRunOptions } from "./loop/types
|
|
|
2
2
|
export type { CuaAction, CuaExecutor, CuaLiveMetadata, CuaLoopOptions, CuaLoopResult, CuaObservation, CuaProvider, CuaSafetyCheck, CuaSpendGate, CuaTurn, CuaTurnRequest, ShortenedWait, } from "./loop/types.js";
|
|
3
3
|
export { describeCuaAction } from "./loop/actions.js";
|
|
4
4
|
export { stableProgressKey } from "./loop/backstop.js";
|
|
5
|
-
export { validClosingReport } from "./loop/debrief.js";
|
|
5
|
+
export { validClosingReport, validImpressionsReply } from "./loop/debrief.js";
|
|
6
|
+
export { IMPRESSIONS_ASK } from "./loop/impressions.js";
|
|
6
7
|
/**
|
|
7
8
|
* Drive the loop to one explicit completion and return its CuaLoopResult: status, completion
|
|
8
9
|
* reason, public reason and ActorTrace. Model-authored text passes through redactNarration before
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { ComputerUseProviderError } from "./provider-error.js";
|
|
2
2
|
import { runActionBatch } from "./loop/actions.js";
|
|
3
3
|
import { advanceBackstop, startBackstop } from "./loop/backstop.js";
|
|
4
|
-
import { requestDebrief } from "./loop/debrief.js";
|
|
4
|
+
import { requestDebrief, validClosingReport } from "./loop/debrief.js";
|
|
5
5
|
import * as stops from "./loop/ending.js";
|
|
6
6
|
import { declaredOutcomeOf } from "./loop/ending.js";
|
|
7
|
+
import { notCollected, recordImpressions } from "./loop/impressions.js";
|
|
7
8
|
import { DesktopObserver } from "./loop/observation.js";
|
|
8
9
|
import { retryAfterOutputLimit } from "./loop/output-limit.js";
|
|
9
10
|
import { requestTurn } from "./loop/provider-call.js";
|
|
@@ -13,7 +14,8 @@ import { accountBillingConflicts } from "./loop/usage.js";
|
|
|
13
14
|
import { loopResult, notice } from "./loop/trace.js";
|
|
14
15
|
export { describeCuaAction } from "./loop/actions.js";
|
|
15
16
|
export { stableProgressKey } from "./loop/backstop.js";
|
|
16
|
-
export { validClosingReport } from "./loop/debrief.js";
|
|
17
|
+
export { validClosingReport, validImpressionsReply } from "./loop/debrief.js";
|
|
18
|
+
export { IMPRESSIONS_ASK } from "./loop/impressions.js";
|
|
17
19
|
/**
|
|
18
20
|
* Drive the loop to one explicit completion and return its CuaLoopResult: status, completion
|
|
19
21
|
* reason, public reason and ActorTrace. Model-authored text passes through redactNarration before
|
|
@@ -118,8 +120,18 @@ async function runTurns(session, conversation) {
|
|
|
118
120
|
// The declared outcome is kept even if redacting the participant's summary fails.
|
|
119
121
|
session.declaredOutcome = declaredOutcomeOf(turn);
|
|
120
122
|
const ended = stops.participantEnded(turn, session.declaredOutcome, (text) => session.redactNarration(text));
|
|
123
|
+
const report = validClosingReport(turn.closingReport) ? turn.closingReport : undefined;
|
|
124
|
+
// A provider that can ask for impressions alone does so after the session, like a debrief.
|
|
125
|
+
if (report?.impressions !== undefined || session.provider.requestImpressions === undefined) {
|
|
126
|
+
session.impressions =
|
|
127
|
+
report === undefined
|
|
128
|
+
? notCollected("the participant ended the session without a structured closing account")
|
|
129
|
+
: recordImpressions(session, report.impressions);
|
|
130
|
+
}
|
|
121
131
|
await observer.observeFinalTasks(turnNumber);
|
|
122
|
-
return
|
|
132
|
+
return session.impressions === undefined
|
|
133
|
+
? { ...ended, debriefTrigger: { kind: "participant_end", observation } }
|
|
134
|
+
: ended;
|
|
123
135
|
}
|
|
124
136
|
const batch = await runActionBatch(session, turn.actions, turn.shortenedWaits);
|
|
125
137
|
conversation.previousExecution = batch.execution;
|