humanish 0.114.0 → 0.115.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 (82) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/CONTEXT.md +1 -1
  3. package/dist/actors/closing-report-limits.d.ts +11 -0
  4. package/dist/actors/closing-report-limits.js +13 -0
  5. package/dist/actors/closing-report.d.ts +68 -0
  6. package/dist/actors/closing-report.js +55 -0
  7. package/dist/actors/codex/restricted-participant-policy.d.ts +4 -47
  8. package/dist/actors/codex/restricted-participant-policy.js +12 -40
  9. package/dist/actors/computer-use/loop/debrief.d.ts +1 -10
  10. package/dist/actors/computer-use/loop/debrief.js +10 -46
  11. package/dist/actors/computer-use/loop/impressions.d.ts +2 -2
  12. package/dist/actors/computer-use/loop/impressions.js +6 -1
  13. package/dist/actors/computer-use/loop.d.ts +0 -1
  14. package/dist/actors/computer-use/loop.js +3 -3
  15. package/dist/actors/computer-use/openai-conversation.d.ts +1 -1
  16. package/dist/actors/computer-use/openai-conversation.js +3 -29
  17. package/dist/actors/computer-use/openai-provider.js +11 -5
  18. package/dist/actors/contract.d.ts +2 -20
  19. package/dist/actors/contract.js +0 -13
  20. package/dist/analysis/admission.d.ts +14 -4
  21. package/dist/analysis/admission.js +14 -9
  22. package/dist/analysis/execute.d.ts +5 -0
  23. package/dist/analysis/execute.js +3 -0
  24. package/dist/analysis/finding-lead.d.ts +27 -0
  25. package/dist/analysis/finding-lead.js +23 -0
  26. package/dist/analysis/service.js +5 -3
  27. package/dist/cli/commands/notes.js +4 -3
  28. package/dist/cli/findings.js +10 -32
  29. package/dist/cli/io.js +4 -7
  30. package/dist/cli/telemetry.js +10 -22
  31. package/dist/cli/update-check.js +6 -18
  32. package/dist/cli/user-config.d.ts +11 -0
  33. package/dist/cli/user-config.js +25 -0
  34. package/dist/comms/email-catch.js +7 -31
  35. package/dist/evidence/encoded-text.d.ts +12 -11
  36. package/dist/evidence/encoded-text.js +81 -70
  37. package/dist/evidence/redaction.d.ts +11 -0
  38. package/dist/evidence/redaction.js +21 -10
  39. package/dist/evidence/secret-scrub.d.ts +14 -1
  40. package/dist/evidence/secret-scrub.js +108 -32
  41. package/dist/feedback/draft.d.ts +1 -1
  42. package/dist/feedback/draft.js +13 -16
  43. package/dist/feedback/export-bundle.js +3 -3
  44. package/dist/feedback/export.js +32 -46
  45. package/dist/feedback/feedback.js +6 -19
  46. package/dist/keys/key-resolution.js +12 -17
  47. package/dist/observer/artifact.d.ts +1 -1
  48. package/dist/observer/http.d.ts +7 -0
  49. package/dist/observer/http.js +19 -1
  50. package/dist/observer/notes-route.d.ts +0 -1
  51. package/dist/observer/notes-route.js +24 -44
  52. package/dist/observer/pinned-files.js +4 -19
  53. package/dist/observer/render.js +3 -2
  54. package/dist/observer/run-routes.js +13 -28
  55. package/dist/observer/serve.js +2 -1
  56. package/dist/observer-app.html +3 -3
  57. package/dist/run/contained-output.d.ts +6 -5
  58. package/dist/run/contained-output.js +19 -26
  59. package/dist/run/evidence-files.d.ts +7 -0
  60. package/dist/run/evidence-files.js +24 -14
  61. package/dist/run/note-files.d.ts +26 -45
  62. package/dist/run/note-files.js +86 -58
  63. package/dist/run/note-shape.d.ts +35 -0
  64. package/dist/run/note-shape.js +20 -0
  65. package/dist/run/notes.d.ts +3 -6
  66. package/dist/run/notes.js +18 -24
  67. package/dist/run/participant-records.d.ts +2 -0
  68. package/dist/run/participant-records.js +17 -2
  69. package/dist/run/transient-comms-secrets.d.ts +6 -5
  70. package/dist/run/transient-comms-secrets.js +19 -24
  71. package/dist/study/summary.d.ts +4 -5
  72. package/dist/study/summary.js +4 -2
  73. package/dist/study/warnings.js +2 -1
  74. package/dist/tui-app.js +1 -1
  75. package/docs/architecture/actor-contract.md +3 -3
  76. package/docs/architecture/restricted-codex-analysis.md +1 -1
  77. package/docs/architecture/state-driven-executor.md +1 -1
  78. package/docs/contracts/run-bundle.md +7 -3
  79. package/docs/contracts/schemas.md +12 -12
  80. package/docs/contracts/study-analysis.md +12 -4
  81. package/package.json +7 -6
  82. package/skills/humanish/SKILL.md +1 -2
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.115.0: Closing report limits, admitted analysis cost, concurrent reviewer notes, known-value scrub (2026-10-08)
12
+
13
+ humanish 0.115.0 asks the OpenAI computer-use participant for its closing report and impressions
14
+ within the limits its reply is checked against, keeps every reviewer note added at the same time,
15
+ and finds more encoded forms of a registered secret. The participant's request schema now carries
16
+ the limits the parser already enforced: a summary of up to 4,000 characters, up to 8 friction
17
+ reports of up to 2,000 characters and up to 6 impressions of up to 500 characters, so a longer
18
+ reply is no longer requested and then dropped. `analyze --json` adds `admission.admittedCostUsd`,
19
+ the smallest `--max-cost` that admits the analysis, and `humanish analyze` names its refusal
20
+ command as `npx humanish analyze` where humanish is a dev dependency. A reviewer note is created
21
+ under its own name, so notes added together from the Observer and `humanish notes --add` are all
22
+ kept. `export --local-only` keeps a `blocked` run `blocked` when its analysis fails the export's
23
+ own check. The known-value scrub exempts only the markers humanish writes, reads text in the same
24
+ five ways as `verify`, and finds a value that overlaps itself in time linear in the text. The TUI's
25
+ study screen shows the expected analysis cost line that `study check` prints, telemetry reads
26
+ `CI=false` as off, and `humanish keys set constructor` is refused with the list of provider keys.
27
+
28
+ [Release notes](https://github.com/danielgwilson/humanish/releases/tag/v0.115.0)
29
+
11
30
  ## 0.114.0: Reviewer notes, expected analysis cost, percent-encoded UTF-8 in verify (2026-10-08)
12
31
 
13
32
  humanish 0.114.0 lets a reviewer note a moment of a recorded run, admits post-run analysis on its
package/CONTEXT.md CHANGED
@@ -41,7 +41,7 @@ bundle and output fields keep older spellings; the second table maps them.
41
41
  | analysis | An independent model reading of a finished run's retained evidence, kept apart from participant feedback. Supported live runs request it automatically, and `humanish analyze` runs it on demand. It records findings and observations as `humanish.study-analysis.v1`. | `src/analysis/`; `runAutomaticAnalysis` in `src/analysis/automatic.ts`; `analyzeRun` in `src/analysis/service.ts`; `ANALYSIS_SCHEMA` in `src/analysis/types.ts` |
42
42
  | finding | A result of analysis, with citations to evidence in the run. Each finding groups the observations that support it. | `src/analysis/` |
43
43
  | observation | One claim in analysis with its basis, evidence ids and limitation. `concernReviews[]` gives each material concern's observation a disposition: `finding`, `context` or `unsupported`. | `src/analysis/` |
44
- | reviewer note | Free text a person adds at a moment of a recorded run, for one participant or the whole run, kept in the run directory as its own file, `notes/<id>.json` (`humanish.run-note.v1`). It is not participant feedback or analysis. `humanish notes` and an Observer served on loopback add notes. | `addRunNote` in `src/run/notes.ts`; `readRunNotes` in `src/run/note-files.ts`; `createNotesWriter` in `src/observer/notes-route.ts` |
44
+ | reviewer note | Free text a person adds at a moment of a recorded run, for one participant or the whole run, kept in the run directory as its own file, `notes/<id>.json` (`humanish.run-note.v1`). It is not participant feedback or analysis. `humanish notes` and an Observer served on loopback add notes. | `RunNote` and its limits in `src/run/note-shape.ts`; `addRunNote` in `src/run/notes.ts`; `readRunNotes` in `src/run/note-files.ts`; `createNotesWriter` in `src/observer/notes-route.ts` |
45
45
 
46
46
  ## Read these field spellings as the concept they name
47
47
 
@@ -0,0 +1,11 @@
1
+ /**
2
+ * How much a participant's closing report may hold. Participants are asked for no more, and a
3
+ * reply that holds more is rejected. Characters are counted as `String.length` counts them.
4
+ */
5
+ export declare const CLOSING_REPORT_LIMITS: Readonly<{
6
+ summaryChars: 4000;
7
+ frictionReports: 8;
8
+ frictionReportChars: 2000;
9
+ impressions: 6;
10
+ impressionChars: 500;
11
+ }>;
@@ -0,0 +1,13 @@
1
+ // The closing report's limits, kept apart from its zod schema in closing-report.ts so the
2
+ // Observer can read them: it bundles this module, and zod stays out of its page.
3
+ /**
4
+ * How much a participant's closing report may hold. Participants are asked for no more, and a
5
+ * reply that holds more is rejected. Characters are counted as `String.length` counts them.
6
+ */
7
+ export const CLOSING_REPORT_LIMITS = Object.freeze({
8
+ summaryChars: 4000,
9
+ frictionReports: 8,
10
+ frictionReportChars: 2000,
11
+ impressions: 6,
12
+ impressionChars: 500,
13
+ });
@@ -0,0 +1,68 @@
1
+ import { z } from "zod";
2
+ declare const impressionSchema: z.ZodObject<{
3
+ kind: z.ZodEnum<{
4
+ liked: "liked";
5
+ missing: "missing";
6
+ unclear: "unclear";
7
+ unfinished: "unfinished";
8
+ unlike_my_work: "unlike_my_work";
9
+ untrustworthy: "untrustworthy";
10
+ }>;
11
+ text: z.ZodString;
12
+ }, z.core.$strict>;
13
+ /** The impressions a participant gave; an empty list means it had none. */
14
+ export declare const participantImpressionsSchema: z.ZodArray<z.ZodObject<{
15
+ kind: z.ZodEnum<{
16
+ liked: "liked";
17
+ missing: "missing";
18
+ unclear: "unclear";
19
+ unfinished: "unfinished";
20
+ unlike_my_work: "unlike_my_work";
21
+ untrustworthy: "untrustworthy";
22
+ }>;
23
+ text: z.ZodString;
24
+ }, z.core.$strict>>;
25
+ /**
26
+ * A closing report. A report without impressions is still valid: third-party provider ports and
27
+ * stored traces do not have them.
28
+ */
29
+ export declare const closingReportSchema: z.ZodObject<{
30
+ summary: z.ZodString;
31
+ frictionReports: z.ZodArray<z.ZodString>;
32
+ impressions: z.ZodOptional<z.ZodArray<z.ZodObject<{
33
+ kind: z.ZodEnum<{
34
+ liked: "liked";
35
+ missing: "missing";
36
+ unclear: "unclear";
37
+ unfinished: "unfinished";
38
+ unlike_my_work: "unlike_my_work";
39
+ untrustworthy: "untrustworthy";
40
+ }>;
41
+ text: z.ZodString;
42
+ }, z.core.$strict>>>;
43
+ }, z.core.$strict>;
44
+ /** The reply to an impressions-only request: the impressions and nothing else. */
45
+ export declare const impressionsReplySchema: z.ZodObject<{
46
+ impressions: z.ZodArray<z.ZodObject<{
47
+ kind: z.ZodEnum<{
48
+ liked: "liked";
49
+ missing: "missing";
50
+ unclear: "unclear";
51
+ unfinished: "unfinished";
52
+ unlike_my_work: "unlike_my_work";
53
+ untrustworthy: "untrustworthy";
54
+ }>;
55
+ text: z.ZodString;
56
+ }, z.core.$strict>>;
57
+ }, z.core.$strict>;
58
+ /** The participant's account, not an independently confirmed product diagnosis. */
59
+ export type ParticipantClosingReport = z.infer<typeof closingReportSchema>;
60
+ /** One first-person impression about something the participant saw in the session. */
61
+ export type ParticipantImpression = z.infer<typeof impressionSchema>;
62
+ /**
63
+ * The JSON Schema a strict structured-output request sends for `schema`. Strict mode needs every
64
+ * field, so optional ones become required. `$schema` is left out, so the request carries only
65
+ * keywords both providers' strict modes are known to accept.
66
+ */
67
+ export declare function strictOutputSchema(schema: z.ZodObject): Record<string, unknown>;
68
+ export {};
@@ -0,0 +1,55 @@
1
+ import { z } from "zod";
2
+ import { CLOSING_REPORT_LIMITS as LIMITS } from "./closing-report-limits.js";
3
+ // What a participant says at the end of a session: the closing report and its impressions. This
4
+ // schema is the one description of them. The providers send the JSON Schema made from it, and
5
+ // every reply is parsed with it, so a participant is asked for exactly what is accepted.
6
+ /**
7
+ * What a participant can say about the product at the end of a session, beyond friction.
8
+ * `unlike_my_work` compares the screen with how the persona does the same task in its own work or
9
+ * life.
10
+ */
11
+ const PARTICIPANT_IMPRESSION_KINDS = [
12
+ "unclear",
13
+ "unfinished",
14
+ "untrustworthy",
15
+ "liked",
16
+ "missing",
17
+ "unlike_my_work",
18
+ ];
19
+ /**
20
+ * Text the participant wrote. The JSON Schema carries the length limits; the check that it is not
21
+ * only whitespace runs on the reply.
22
+ */
23
+ const said = (maxChars) => z
24
+ .string()
25
+ .min(1)
26
+ .max(maxChars)
27
+ .refine((text) => text.trim() !== "");
28
+ const impressionSchema = z.strictObject({
29
+ kind: z.enum(PARTICIPANT_IMPRESSION_KINDS),
30
+ text: said(LIMITS.impressionChars),
31
+ });
32
+ /** The impressions a participant gave; an empty list means it had none. */
33
+ export const participantImpressionsSchema = z.array(impressionSchema).max(LIMITS.impressions);
34
+ /**
35
+ * A closing report. A report without impressions is still valid: third-party provider ports and
36
+ * stored traces do not have them.
37
+ */
38
+ export const closingReportSchema = z.strictObject({
39
+ summary: said(LIMITS.summaryChars),
40
+ frictionReports: z.array(said(LIMITS.frictionReportChars)).max(LIMITS.frictionReports),
41
+ impressions: participantImpressionsSchema.optional(),
42
+ });
43
+ /** The reply to an impressions-only request: the impressions and nothing else. */
44
+ export const impressionsReplySchema = z.strictObject({
45
+ impressions: participantImpressionsSchema,
46
+ });
47
+ /**
48
+ * The JSON Schema a strict structured-output request sends for `schema`. Strict mode needs every
49
+ * field, so optional ones become required. `$schema` is left out, so the request carries only
50
+ * keywords both providers' strict modes are known to accept.
51
+ */
52
+ export function strictOutputSchema(schema) {
53
+ const { $schema: _dialect, ...json } = z.toJSONSchema(schema.required(), { io: "input" });
54
+ return json;
55
+ }
@@ -1,5 +1,6 @@
1
- import { type CuaTurn } from "../computer-use/loop.js";
2
- import { type ActorExecutionProfile, type ParticipantClosingReport } from "../contract.js";
1
+ import { type ParticipantClosingReport } from "../closing-report.js";
2
+ import type { CuaTurn } from "../computer-use/loop.js";
3
+ import type { ActorExecutionProfile } from "../contract.js";
3
4
  /** The declared profile. A participant replaces cliVersion with its detected CLI release, or with
4
5
  * this host's newest qualified release before its first launch. */
5
6
  export declare const PARTICIPANT_PROFILE: Readonly<ActorExecutionProfile>;
@@ -16,51 +17,7 @@ export declare const PARTICIPANT_LIMITS: Readonly<{
16
17
  }>;
17
18
  export declare function participantToolSchema(speechEnabled?: boolean): Record<string, unknown>;
18
19
  export declare const PARTICIPANT_TOOL_SCHEMA: Record<string, unknown>;
19
- export declare const PARTICIPANT_FINAL_SCHEMA: {
20
- type: string;
21
- additionalProperties: boolean;
22
- required: string[];
23
- properties: {
24
- outcome: {
25
- type: string;
26
- enum: string[];
27
- };
28
- summary: {
29
- type: string;
30
- minLength: number;
31
- maxLength: number;
32
- };
33
- frictionReports: {
34
- type: string;
35
- maxItems: number;
36
- items: {
37
- type: string;
38
- minLength: number;
39
- maxLength: number;
40
- };
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
- };
62
- };
63
- };
20
+ export declare const PARTICIPANT_FINAL_SCHEMA: Record<string, unknown>;
64
21
  /** humanish tool arguments, validated before the shared executor sees a batch. */
65
22
  export declare function parseParticipantTool(value: unknown, speechEnabled?: boolean): CuaTurn;
66
23
  export declare function parseParticipantFinal(value: unknown): CuaTurn & {
@@ -1,7 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { BROWSER_CONTROL_LIMITS, browserControlActionSchema, browserOnlyControlActionSchema, validateBrowserControlAction, } from "../../browser-control/protocol.js";
3
- import { validClosingReport } from "../computer-use/loop.js";
4
- import { PARTICIPANT_IMPRESSION_KINDS, } from "../contract.js";
3
+ import { closingReportSchema, strictOutputSchema, } from "../closing-report.js";
5
4
  /** The declared profile. A participant replaces cliVersion with its detected CLI release, or with
6
5
  * this host's newest qualified release before its first launch. */
7
6
  export const PARTICIPANT_PROFILE = Object.freeze({
@@ -38,33 +37,12 @@ export function participantToolSchema(speechEnabled = false) {
38
37
  return z.toJSONSchema(toolInput(speechEnabled), { io: "input" });
39
38
  }
40
39
  export const PARTICIPANT_TOOL_SCHEMA = participantToolSchema();
41
- export const PARTICIPANT_FINAL_SCHEMA = {
42
- type: "object",
43
- additionalProperties: false,
44
- required: ["outcome", "summary", "frictionReports", "impressions"],
45
- properties: {
46
- outcome: { type: "string", enum: ["reached", "not_reached", "blocked"] },
47
- summary: { type: "string", minLength: 1, maxLength: 4000 },
48
- frictionReports: {
49
- type: "array",
50
- maxItems: 8,
51
- items: { type: "string", minLength: 1, maxLength: 2000 },
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
- },
66
- },
67
- };
40
+ /** The participant's final account: how the task went, then its closing report. */
41
+ const participantFinal = z.strictObject({
42
+ outcome: z.enum(["reached", "not_reached", "blocked"]),
43
+ ...closingReportSchema.shape,
44
+ });
45
+ export const PARTICIPANT_FINAL_SCHEMA = strictOutputSchema(participantFinal);
68
46
  /**
69
47
  * Shorten each wait longer than one browser-control request may wait. A failed tool call ends the
70
48
  * native run, so a wait that asks too much would otherwise end the whole session.
@@ -107,22 +85,16 @@ export function parseParticipantTool(value, speechEnabled = false) {
107
85
  };
108
86
  }
109
87
  export function parseParticipantFinal(value) {
110
- if (!value || typeof value !== "object" || Array.isArray(value))
111
- throw new Error("invalid_response");
112
- const { outcome, ...report } = value;
113
- if ((outcome !== "reached" && outcome !== "not_reached" && outcome !== "blocked") ||
114
- !validClosingReport(report))
88
+ const final = participantFinal.safeParse(value);
89
+ if (!final.success)
115
90
  throw new Error("invalid_response");
91
+ const { outcome, ...closingReport } = final.data;
116
92
  return {
117
93
  actions: [],
118
94
  pendingSafetyChecks: [],
119
95
  done: true,
120
96
  outcome,
121
- message: [report.summary, ...report.frictionReports].join("\n"),
122
- closingReport: {
123
- summary: report.summary,
124
- frictionReports: [...report.frictionReports],
125
- ...(report.impressions === undefined ? {} : { impressions: [...report.impressions] }),
126
- },
97
+ message: [closingReport.summary, ...closingReport.frictionReports].join("\n"),
98
+ closingReport,
127
99
  };
128
100
  }
@@ -1,4 +1,4 @@
1
- import { type ActorTrace, type ParticipantClosingReport, type ParticipantImpression } from "../../contract.js";
1
+ import type { ActorTrace } 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,15 +9,6 @@ export interface DebriefContext {
9
9
  readonly previousExecution: CuaTurnRequest["previousExecution"];
10
10
  readonly acknowledgedSafetyChecks: CuaTurnRequest["acknowledgedSafetyChecks"];
11
11
  }
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
- */
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
- };
21
12
  /**
22
13
  * Ask the participant for its closing report after a structured stop, or for its impressions only
23
14
  * after it ended the session itself, or record why the request was skipped. Either way the result
@@ -1,4 +1,4 @@
1
- import { PARTICIPANT_IMPRESSION_KINDS, } from "../../contract.js";
1
+ import { closingReportSchema, participantImpressionsSchema } from "../../closing-report.js";
2
2
  import { isComputerUseAdmissionLimitError } from "../admission-limit.js";
3
3
  import { IMPRESSIONS_ASK, IMPRESSIONS_HINT, recordImpressions } from "./impressions.js";
4
4
  import { singleDispatch } from "./provider-call.js";
@@ -7,45 +7,6 @@ import { notice } from "./trace.js";
7
7
  import { isCompleteTurnUsage } from "./usage.js";
8
8
  const DEBRIEF_REQUEST_CAP_MS = 30_000;
9
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
- */
14
- export function validClosingReport(value) {
15
- if (value === null || typeof value !== "object" || Array.isArray(value))
16
- return false;
17
- const report = value;
18
- return (Object.keys(report).every((key) => REPORT_KEYS.includes(key)) &&
19
- typeof report.summary === "string" &&
20
- report.summary.trim().length > 0 &&
21
- report.summary.length <= 4_000 &&
22
- Array.isArray(report.frictionReports) &&
23
- report.frictionReports.length <= 8 &&
24
- report.frictionReports.every((item) => typeof item === "string" && item.trim().length > 0 && item.length <= 2_000) &&
25
- (report.impressions === undefined || validImpressions(report.impressions)));
26
- }
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
- }));
48
- }
49
10
  /**
50
11
  * Ask the participant for its closing report after a structured stop, or for its impressions only
51
12
  * after it ended the session itself, or record why the request was skipped. Either way the result
@@ -171,22 +132,25 @@ function acceptDebriefTurn(session, turn, trigger, record) {
171
132
  if (turn.actions.length > 0 || turn.pendingSafetyChecks.length > 0) {
172
133
  return record("failed", "the closing response requested actions or safety checks; none were executed and its report was not accepted", usageReported);
173
134
  }
135
+ // First-party providers parse their replies already; these parses cover third-party ports.
174
136
  if (trigger.kind === "participant_end") {
175
- if (!validImpressions(turn.impressions)) {
137
+ const impressions = participantImpressionsSchema.safeParse(turn.impressions);
138
+ if (!impressions.success) {
176
139
  return record("failed", turn.interruption === "output_limit"
177
140
  ? "the reply was cut off by the output limit"
178
141
  : "the reply did not contain valid impressions", usageReported);
179
142
  }
180
- session.impressions = recordImpressions(session, turn.impressions);
143
+ session.impressions = recordImpressions(session, impressions.data);
181
144
  return record("completed", "one read-only impressions request; no additional desktop actions; the participant's own ending is unchanged", usageReported);
182
145
  }
183
- if (!validClosingReport(turn.closingReport)) {
146
+ const closing = closingReportSchema.safeParse(turn.closingReport);
147
+ if (!closing.success) {
184
148
  return record("failed", "the closing response did not contain a valid structured participant report", usageReported);
185
149
  }
186
150
  const report = {
187
- summary: session.redactNarration(turn.closingReport.summary.trim()),
151
+ summary: session.redactNarration(closing.data.summary.trim()),
188
152
  frictionReports: [
189
- ...new Set(turn.closingReport.frictionReports.map((text) => session.redactNarration(text.trim()))),
153
+ ...new Set(closing.data.frictionReports.map((text) => session.redactNarration(text.trim()))),
190
154
  ],
191
155
  };
192
156
  const messageId = session.trace.record("message", () => ({
@@ -195,7 +159,7 @@ function acceptDebriefTurn(session, turn, trigger, record) {
195
159
  text: [report.summary, ...report.frictionReports].join("\n\n"),
196
160
  }));
197
161
  session.trace.bump("messages");
198
- session.impressions = recordImpressions(session, turn.closingReport.impressions);
162
+ session.impressions = recordImpressions(session, closing.data.impressions);
199
163
  const debrief = record("completed", "one read-only report; no additional desktop actions; original stop and task outcomes preserved", usageReported);
200
164
  return { ...debrief, report, messageId };
201
165
  }
@@ -1,9 +1,9 @@
1
1
  import type { ActorTrace, ParticipantImpression, ParticipantImpressions } from "../../contract.js";
2
2
  import type { LoopSession } from "./session.js";
3
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.";
4
+ export declare const IMPRESSIONS_ASK: string;
5
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.";
6
+ export declare const IMPRESSIONS_HINT: string;
7
7
  /**
8
8
  * Record a closing account's impressions as quotable participant messages. They answer the
9
9
  * harness's question, so they leave counts.messages, which the hollow-completion rule reads, alone.
@@ -1,8 +1,13 @@
1
+ import { CLOSING_REPORT_LIMITS } from "../../closing-report-limits.js";
1
2
  // A participant's impressions: what it says about the product at the end of a session, beyond
2
3
  // friction. Each one is recorded as its own message so the analysis can quote and cite it as a
3
4
  // participant statement, and the trace says why none were collected when a session has none.
5
+ // The limits are spelled out in the ask, so the participant is told the numbers its reply is
6
+ // checked against. Counts up to ten are written as words.
7
+ const COUNT_WORDS = "zero one two three four five six seven eight nine ten".split(" ");
8
+ const inWords = (count) => COUNT_WORDS[count] ?? String(count);
4
9
  /** 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.";
10
+ export const IMPRESSIONS_ASK = `In impressions, list up to ${inWords(CLOSING_REPORT_LIMITS.impressions)} 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 ${CLOSING_REPORT_LIMITS.impressionChars} characters. Use an empty list if you have none.`;
6
11
  /** The request after the participant ended the session itself without giving impressions. */
7
12
  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
13
  /**
@@ -2,7 +2,6 @@ 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, validImpressionsReply } from "./loop/debrief.js";
6
5
  export { IMPRESSIONS_ASK } from "./loop/impressions.js";
7
6
  /**
8
7
  * Drive the loop to one explicit completion and return its CuaLoopResult: status, completion
@@ -1,7 +1,8 @@
1
+ import { closingReportSchema } from "../closing-report.js";
1
2
  import { ComputerUseProviderError } from "./provider-error.js";
2
3
  import { runActionBatch } from "./loop/actions.js";
3
4
  import { advanceBackstop, startBackstop } from "./loop/backstop.js";
4
- import { requestDebrief, validClosingReport } from "./loop/debrief.js";
5
+ import { requestDebrief } from "./loop/debrief.js";
5
6
  import * as stops from "./loop/ending.js";
6
7
  import { declaredOutcomeOf } from "./loop/ending.js";
7
8
  import { notCollected, recordImpressions } from "./loop/impressions.js";
@@ -14,7 +15,6 @@ import { accountBillingConflicts } from "./loop/usage.js";
14
15
  import { loopResult, notice } from "./loop/trace.js";
15
16
  export { describeCuaAction } from "./loop/actions.js";
16
17
  export { stableProgressKey } from "./loop/backstop.js";
17
- export { validClosingReport, validImpressionsReply } from "./loop/debrief.js";
18
18
  export { IMPRESSIONS_ASK } from "./loop/impressions.js";
19
19
  /**
20
20
  * Drive the loop to one explicit completion and return its CuaLoopResult: status, completion
@@ -120,7 +120,7 @@ async function runTurns(session, conversation) {
120
120
  // The declared outcome is kept even if redacting the participant's summary fails.
121
121
  session.declaredOutcome = declaredOutcomeOf(turn);
122
122
  const ended = stops.participantEnded(turn, session.declaredOutcome, (text) => session.redactNarration(text));
123
- const report = validClosingReport(turn.closingReport) ? turn.closingReport : undefined;
123
+ const report = closingReportSchema.safeParse(turn.closingReport).data;
124
124
  // A provider that can ask for impressions alone does so after the session, like a debrief.
125
125
  if (report?.impressions !== undefined || session.provider.requestImpressions === undefined) {
126
126
  session.impressions =
@@ -1,6 +1,6 @@
1
1
  import type { CuaTurn, CuaTurnRequest } from "./loop.js";
2
2
  import type { ReasoningEffort } from "../reasoning-effort.js";
3
- import { type ActorConversation } from "../contract.js";
3
+ import type { ActorConversation } from "../contract.js";
4
4
  import { type OpenAiReasoningSummary } from "./openai-wire.js";
5
5
  /** How a conversation starts. The model settings stay fixed for the whole session. */
6
6
  export interface OpenAiConversationSettings {
@@ -1,4 +1,4 @@
1
- import { PARTICIPANT_IMPRESSION_KINDS } from "../contract.js";
1
+ import { closingReportSchema, impressionsReplySchema, strictOutputSchema, } from "../closing-report.js";
2
2
  import { CarriedConversation, estimateTokens } from "./openai-context.js";
3
3
  import { buildCallOutput, buildContinuationRequest, buildInitialRequest, hintItems, openingMessage, parseOpenAiResponse, } from "./openai-wire.js";
4
4
  /**
@@ -6,44 +6,18 @@ import { buildCallOutput, buildContinuationRequest, buildInitialRequest, hintIte
6
6
  * counts reasoning too, and leaves room for six impressions after the summary and friction reports.
7
7
  */
8
8
  const CLOSING_OUTPUT_LIMIT = 3072;
9
- const IMPRESSIONS_SCHEMA = {
10
- type: "array",
11
- items: {
12
- type: "object",
13
- additionalProperties: false,
14
- required: ["kind", "text"],
15
- properties: {
16
- kind: { type: "string", enum: [...PARTICIPANT_IMPRESSION_KINDS] },
17
- text: { type: "string" },
18
- },
19
- },
20
- };
21
9
  const CLOSING_FORMATS = {
22
10
  report: {
23
11
  type: "json_schema",
24
12
  name: "participant_closing_report",
25
13
  strict: true,
26
- schema: {
27
- type: "object",
28
- additionalProperties: false,
29
- required: ["summary", "frictionReports", "impressions"],
30
- properties: {
31
- summary: { type: "string" },
32
- frictionReports: { type: "array", items: { type: "string" } },
33
- impressions: IMPRESSIONS_SCHEMA,
34
- },
35
- },
14
+ schema: strictOutputSchema(closingReportSchema),
36
15
  },
37
16
  impressions: {
38
17
  type: "json_schema",
39
18
  name: "participant_impressions",
40
19
  strict: true,
41
- schema: {
42
- type: "object",
43
- additionalProperties: false,
44
- required: ["impressions"],
45
- properties: { impressions: IMPRESSIONS_SCHEMA },
46
- },
20
+ schema: strictOutputSchema(impressionsReplySchema),
47
21
  },
48
22
  };
49
23
  export class OpenAiConversation {
@@ -1,4 +1,4 @@
1
- import { validClosingReport, validImpressionsReply } from "./loop.js";
1
+ import { closingReportSchema, impressionsReplySchema } from "../closing-report.js";
2
2
  import { ComputerUseAdmissionLimitError, isComputerUseAdmissionLimitError, } from "./admission-limit.js";
3
3
  import { ComputerUsePromptRefusedError } from "./provider-error.js";
4
4
  import { OpenAiConversation, } from "./openai-conversation.js";
@@ -469,10 +469,16 @@ export function createOpenAiResponsesProvider(options) {
469
469
  try {
470
470
  const reply = JSON.parse(turn.message ?? "");
471
471
  if (asRecord(raw).status === "completed") {
472
- if (closing === "report" && validClosingReport(reply))
473
- return { ...turn, closingReport: reply };
474
- if (closing === "impressions" && validImpressionsReply(reply))
475
- return { ...turn, impressions: reply.impressions };
472
+ if (closing === "report") {
473
+ const report = closingReportSchema.safeParse(reply);
474
+ if (report.success)
475
+ return { ...turn, closingReport: report.data };
476
+ }
477
+ else {
478
+ const impressions = impressionsReplySchema.safeParse(reply);
479
+ if (impressions.success)
480
+ return { ...turn, impressions: impressions.data.impressions };
481
+ }
476
482
  }
477
483
  }
478
484
  catch {