humanish 0.140.0 → 0.142.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/ARCHITECTURE.md +1 -0
- package/CHANGELOG.md +23 -0
- package/README.md +2 -0
- package/dist/actors/codex/restricted-participant.js +1 -1
- package/dist/actors/computer-use/external-handoff.d.ts +22 -0
- package/dist/actors/computer-use/external-handoff.js +83 -0
- package/dist/actors/computer-use/loop/observation.d.ts +7 -0
- package/dist/actors/computer-use/loop/observation.js +17 -1
- package/dist/actors/computer-use/loop/types.d.ts +6 -0
- package/dist/analysis/admission.d.ts +8 -0
- package/dist/analysis/admission.js +15 -4
- package/dist/analysis/analysis-limits.d.ts +6 -0
- package/dist/analysis/analysis-limits.js +6 -0
- package/dist/analysis/analysis-lock.d.ts +8 -0
- package/dist/analysis/analysis-lock.js +129 -0
- package/dist/analysis/automatic-completion.js +7 -0
- package/dist/analysis/automatic-config.d.ts +5 -2
- package/dist/analysis/automatic-config.js +23 -8
- package/dist/analysis/automatic.js +13 -7
- package/dist/analysis/execute.d.ts +17 -6
- package/dist/analysis/execute.js +37 -19
- package/dist/analysis/gemini-wire.d.ts +8 -0
- package/dist/analysis/gemini-wire.js +165 -0
- package/dist/analysis/image-tokens.d.ts +4 -2
- package/dist/analysis/image-tokens.js +24 -3
- package/dist/analysis/job.d.ts +2 -0
- package/dist/analysis/job.js +7 -2
- package/dist/analysis/openai-wire.d.ts +5 -0
- package/dist/analysis/openai-wire.js +101 -0
- package/dist/analysis/provider.d.ts +15 -7
- package/dist/analysis/provider.js +21 -101
- package/dist/analysis/recording-ended.d.ts +16 -0
- package/dist/analysis/recording-ended.js +29 -0
- package/dist/analysis/responses.d.ts +3 -0
- package/dist/analysis/responses.js +19 -0
- package/dist/analysis/service.d.ts +1 -3
- package/dist/analysis/service.js +34 -53
- package/dist/analysis/source.d.ts +1 -1
- package/dist/analysis/types.d.ts +8 -2
- package/dist/analysis/validation.d.ts +1 -0
- package/dist/analysis/validation.js +13 -12
- package/dist/browser-control/protocol.d.ts +8 -0
- package/dist/browser-control/protocol.js +21 -0
- package/dist/cli/commands/analysis-signals.d.ts +10 -4
- package/dist/cli/commands/analysis-signals.js +28 -6
- package/dist/cli/commands/analyze.js +9 -7
- package/dist/cli/commands/observe.js +26 -1
- package/dist/cli/commands/run-command.d.ts +1 -1
- package/dist/cli/commands/run-command.js +12 -0
- package/dist/cli/commands/runs.js +8 -2
- package/dist/cli/commands/study-remote.d.ts +13 -0
- package/dist/cli/commands/study-remote.js +125 -0
- package/dist/cli/commands/study-route-computer-use.js +28 -4
- package/dist/cli/commands/study-route-scripted.js +3 -2
- package/dist/cli/commands/study-route-shared-world.js +3 -2
- package/dist/cli/commands/study-route-terminal.js +3 -2
- package/dist/cli/commands/study-run.js +34 -0
- package/dist/cli/commands/watch.js +5 -0
- package/dist/cli/doctor.js +7 -2
- package/dist/cli/findings.d.ts +30 -0
- package/dist/cli/findings.js +114 -15
- package/dist/cli/io.d.ts +8 -0
- package/dist/cli/io.js +3 -1
- package/dist/evidence/browser-hygiene.d.ts +12 -1
- package/dist/evidence/browser-hygiene.js +45 -2
- package/dist/feedback/export-bundle.js +1 -1
- package/dist/guest/external-links.d.ts +14 -0
- package/dist/guest/external-links.js +48 -0
- package/dist/guest/runtime-desktop.js +5 -2
- package/dist/keys/key-resolution.d.ts +10 -0
- package/dist/keys/key-resolution.js +33 -5
- package/dist/observer/data.d.ts +4 -0
- package/dist/observer/listen.d.ts +7 -2
- package/dist/observer/listen.js +10 -4
- package/dist/observer/render.d.ts +2 -0
- package/dist/observer/render.js +1 -1
- package/dist/observer/run-routes.js +12 -3
- package/dist/observer-app.html +2 -2
- package/dist/remote/launch.d.ts +53 -0
- package/dist/remote/launch.js +320 -0
- package/dist/remote/observer-proxy.d.ts +11 -0
- package/dist/remote/observer-proxy.js +66 -0
- package/dist/remote/plan.d.ts +35 -0
- package/dist/remote/plan.js +114 -0
- package/dist/remote/reattach.d.ts +17 -0
- package/dist/remote/reattach.js +81 -0
- package/dist/remote/runner.d.ts +68 -0
- package/dist/remote/runner.js +216 -0
- package/dist/routes/computer-use/e2b-desktop/desktop.js +27 -17
- package/dist/routes/computer-use/e2b-desktop/start.js +1 -1
- package/dist/routes/computer-use/e2b-desktop/teardown.js +1 -1
- package/dist/routes/computer-use/in-process-desktop.js +2 -2
- package/dist/routes/computer-use/live-phase.js +1 -1
- package/dist/routes/computer-use/setup.js +2 -2
- package/dist/routes/computer-use/types.d.ts +2 -0
- package/dist/routes/scripted/types.d.ts +2 -0
- package/dist/routes/shared-world/route.js +1 -1
- package/dist/routes/shared-world/types.d.ts +2 -0
- package/dist/routes/terminal/types.d.ts +2 -0
- package/dist/run/costs.d.ts +2 -1
- package/dist/run/display.d.ts +6 -1
- package/dist/run/display.js +11 -0
- package/dist/run/host-suspension.js +4 -1
- package/dist/run/preflight-receipts.d.ts +3 -12
- package/dist/run/preflight-receipts.js +7 -67
- package/dist/run/pricing.d.ts +13 -1
- package/dist/run/pricing.js +50 -4
- package/dist/run/process-owner.d.ts +28 -0
- package/dist/run/process-owner.js +83 -0
- package/dist/run/projection.js +1 -1
- package/dist/run/reclaim.d.ts +6 -0
- package/dist/run/reclaim.js +32 -2
- package/dist/run/results.d.ts +1 -1
- package/dist/run/route-shell.js +1 -1
- package/dist/run/run-cost.d.ts +2 -1
- package/dist/run/run-cost.js +6 -1
- package/dist/run/run-index.d.ts +3 -1
- package/dist/run/run-index.js +23 -11
- package/dist/run/run.d.ts +29 -5
- package/dist/run/run.js +43 -17
- package/dist/run/sandbox-creates.d.ts +10 -3
- package/dist/run/sandbox-creates.js +48 -6
- package/dist/run/status.d.ts +57 -2
- package/dist/run/status.js +87 -1
- package/dist/run/stored-runs.d.ts +3 -0
- package/dist/run/stored-runs.js +9 -4
- package/dist/run-study.d.ts +5 -0
- package/dist/study/doctor.js +14 -1
- package/dist/study/requirements.d.ts +1 -1
- package/dist/study/requirements.js +8 -6
- package/dist/study/route-inputs.js +4 -0
- package/dist/study/run-study-events.d.ts +8 -2
- package/dist/study/run-study-events.js +9 -2
- package/dist/substrates/e2b/base-module.d.ts +3 -0
- package/dist/substrates/e2b/base-module.js +40 -0
- package/dist/substrates/e2b/desktop-browser.d.ts +1 -1
- package/dist/substrates/e2b/desktop-browser.js +27 -7
- package/dist/substrates/e2b/desktop-executor.d.ts +7 -0
- package/dist/substrates/e2b/desktop-executor.js +3 -0
- package/dist/substrates/e2b/desktop-handoff.d.ts +22 -0
- package/dist/substrates/e2b/desktop-handoff.js +86 -0
- package/dist/substrates/e2b/sdk.d.ts +24 -0
- package/dist/substrates/e2b/sdk.js +5 -1
- package/dist/substrates/e2b/shell-only.d.ts +5 -0
- package/dist/substrates/e2b/shell-only.js +27 -0
- package/dist/substrates/local/runtime-release.js +8 -8
- package/dist/substrates/modal/sandbox.js +27 -28
- package/dist/substrates/modal/sdk.d.ts +7 -0
- package/dist/tui-app.js +115 -115
- package/docs/architecture/observer.md +7 -3
- package/docs/contracts/schemas.md +24 -3
- package/docs/contracts/study-analysis.md +52 -4
- package/docs/product/automatic-analysis.md +55 -9
- package/package.json +2 -2
- package/skills/humanish/SKILL.md +118 -5
|
@@ -13,7 +13,7 @@ export interface AnalysisProviderRequest {
|
|
|
13
13
|
timeoutMs: number;
|
|
14
14
|
signal?: AbortSignal;
|
|
15
15
|
}
|
|
16
|
-
interface AnalysisTokenUsage {
|
|
16
|
+
export interface AnalysisTokenUsage {
|
|
17
17
|
input: number;
|
|
18
18
|
output: number;
|
|
19
19
|
cachedInput?: number;
|
|
@@ -40,10 +40,6 @@ export interface AnalysisProviderResult {
|
|
|
40
40
|
protocolAdditions?: readonly string[];
|
|
41
41
|
}
|
|
42
42
|
export type AnalysisProvider = (request: AnalysisProviderRequest) => Promise<AnalysisProviderResult>;
|
|
43
|
-
/** An input image the provider accepts; the groups are the image type and its base64 bytes. */
|
|
44
|
-
export declare const INPUT_IMAGE_DATA_URL: RegExp;
|
|
45
|
-
/** Text and usage wire shapes reuse the captured closing-report contract; refusal fails closed. */
|
|
46
|
-
export declare function parseAnalysisResponse(raw: unknown): AnalysisProviderResult;
|
|
47
43
|
/** The part of fetch this provider uses; undici's fetch and test doubles both satisfy it. */
|
|
48
44
|
export type AnalysisFetch = (url: string, init: {
|
|
49
45
|
method: "POST";
|
|
@@ -62,9 +58,21 @@ export type AnalysisFetch = (url: string, init: {
|
|
|
62
58
|
status: number;
|
|
63
59
|
body: ReadableStream<Uint8Array> | null;
|
|
64
60
|
}>;
|
|
65
|
-
/**
|
|
61
|
+
/** The analysts humanish calls over HTTP with an API key. Codex runs through its own CLI. */
|
|
62
|
+
export type AnalysisApiVendor = "openai" | "gemini";
|
|
63
|
+
/** One vendor's wire format. The transport sends `body` to `url` with `headers` and gives `parse`
|
|
64
|
+
* the response JSON only after a 2xx status, within the byte cap and the deadline. */
|
|
65
|
+
export interface AnalysisWire {
|
|
66
|
+
/** A fixed endpoint: never derived from env, so evidence and credentials have one destination. */
|
|
67
|
+
url(model: string): string;
|
|
68
|
+
headers(apiKey: string): Record<string, string>;
|
|
69
|
+
body(request: AnalysisProviderRequest): unknown;
|
|
70
|
+
parse(raw: unknown): AnalysisProviderResult;
|
|
71
|
+
}
|
|
72
|
+
/** One request to the vendor's fixed endpoint, never retried and never sent elsewhere. */
|
|
66
73
|
export declare function createAnalysisProvider(options: {
|
|
67
74
|
apiKey: string;
|
|
75
|
+
/** Omitted is OpenAI, the analyst humanish has always called. */
|
|
76
|
+
vendor?: AnalysisApiVendor;
|
|
68
77
|
fetchFn?: AnalysisFetch;
|
|
69
78
|
}): AnalysisProvider;
|
|
70
|
-
export {};
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
+
// The analysis provider seam and the HTTP transport its API adapters share. Each API vendor owns
|
|
2
|
+
// only its wire format (gemini-wire.ts, openai-wire.ts): the endpoint, the request body, and how a
|
|
3
|
+
// response becomes a result. The transport owns what must hold for every vendor: one fixed endpoint
|
|
4
|
+
// per vendor, request and response byte caps, the caller's wall-clock deadline, no retry, and a
|
|
5
|
+
// closed error vocabulary that never carries provider error prose. The Codex account analyst is
|
|
6
|
+
// the third adapter at the same seam (restricted-codex.ts).
|
|
1
7
|
import { Agent, fetch as undiciFetch } from "undici";
|
|
2
|
-
import {
|
|
8
|
+
import { isRecord } from "../run/type-guards.js";
|
|
9
|
+
import { INPUT_IMAGE_DATA_URL } from "./image-tokens.js";
|
|
10
|
+
import { geminiWire } from "./gemini-wire.js";
|
|
11
|
+
import { openaiWire } from "./openai-wire.js";
|
|
3
12
|
const MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
4
13
|
const MAX_REQUEST_BYTES = 32 * 1024 * 1024;
|
|
5
|
-
|
|
6
|
-
export const INPUT_IMAGE_DATA_URL = /^data:image\/(png|jpeg|webp);base64,([A-Za-z0-9+/]+={0,2})$/;
|
|
7
|
-
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
|
|
8
|
-
? value
|
|
9
|
-
: {};
|
|
10
|
-
const count = (value) => Number.isSafeInteger(value) && value >= 0 && value <= 1e12;
|
|
14
|
+
const record = (value) => (isRecord(value) ? value : {});
|
|
11
15
|
/** Undici's fetch has a separate response-header deadline. The caller's wall-clock
|
|
12
16
|
* deadline must govern this one request, including uploads and body reads, without
|
|
13
17
|
* changing transport behavior for other requests in the process. The agent is used with
|
|
@@ -41,72 +45,14 @@ function networkErrorCode(error) {
|
|
|
41
45
|
}
|
|
42
46
|
return "provider_network_error";
|
|
43
47
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const cachedInput = details.cached_tokens;
|
|
50
|
-
const cacheWriteInput = details.cache_write_tokens;
|
|
51
|
-
if (!count(input) ||
|
|
52
|
-
!count(output) ||
|
|
53
|
-
(cachedInput !== undefined && !count(cachedInput)) ||
|
|
54
|
-
(cacheWriteInput !== undefined && !count(cacheWriteInput)) ||
|
|
55
|
-
(cachedInput ?? 0) + (cacheWriteInput ?? 0) >
|
|
56
|
-
input)
|
|
57
|
-
return null;
|
|
58
|
-
return {
|
|
59
|
-
input,
|
|
60
|
-
output,
|
|
61
|
-
...(cachedInput === undefined ? {} : { cachedInput: cachedInput }),
|
|
62
|
-
...(cacheWriteInput === undefined ? {} : { cacheWriteInput: cacheWriteInput }),
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
/** Text and usage wire shapes reuse the captured closing-report contract; refusal fails closed. */
|
|
66
|
-
export function parseAnalysisResponse(raw) {
|
|
67
|
-
const root = record(raw);
|
|
68
|
-
const usage = usageOf(raw);
|
|
69
|
-
const output = Array.isArray(root.output) ? root.output : [];
|
|
70
|
-
const contents = output
|
|
71
|
-
.filter((item) => record(item).type === "message")
|
|
72
|
-
.flatMap((item) => Array.isArray(record(item).content) ? record(item).content : []);
|
|
73
|
-
const base = { output: null, usage, dispatched: true };
|
|
74
|
-
if (contents.some((item) => record(item).type === "refusal")) {
|
|
75
|
-
return { ...base, status: "refused", errorCode: "refusal" };
|
|
76
|
-
}
|
|
77
|
-
if (root.status === "incomplete")
|
|
78
|
-
return { ...base, status: "incomplete", errorCode: "output_incomplete" };
|
|
79
|
-
if (root.status !== "completed" ||
|
|
80
|
-
output.some((item) => !["message", "reasoning"].includes(String(record(item).type)))) {
|
|
81
|
-
return { ...base, status: "failed", errorCode: "invalid_response" };
|
|
82
|
-
}
|
|
83
|
-
// A single JSON answer is expected. Do not concatenate multiple answer objects or expose a
|
|
84
|
-
// refusal, reasoning item, or incomplete message as the report's prose.
|
|
85
|
-
const messages = output.filter((item) => record(item).type === "message");
|
|
86
|
-
const texts = contents.filter((item) => record(item).type === "output_text");
|
|
87
|
-
if (messages.length !== 1 ||
|
|
88
|
-
record(messages[0]).status !== "completed" ||
|
|
89
|
-
record(messages[0]).role !== "assistant" ||
|
|
90
|
-
texts.length !== 1 ||
|
|
91
|
-
contents.length !== 1 ||
|
|
92
|
-
typeof record(texts[0]).text !== "string") {
|
|
93
|
-
return { ...base, status: "failed", errorCode: "invalid_response" };
|
|
94
|
-
}
|
|
95
|
-
try {
|
|
96
|
-
return {
|
|
97
|
-
...base,
|
|
98
|
-
status: "completed",
|
|
99
|
-
output: JSON.parse(record(texts[0]).text),
|
|
100
|
-
errorCode: null,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
catch {
|
|
104
|
-
return { ...base, status: "failed", errorCode: "invalid_response" };
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
/** No alternate endpoint or env-derived base URL: evidence and credentials have one destination. */
|
|
48
|
+
const WIRES = {
|
|
49
|
+
openai: openaiWire,
|
|
50
|
+
gemini: geminiWire,
|
|
51
|
+
};
|
|
52
|
+
/** One request to the vendor's fixed endpoint, never retried and never sent elsewhere. */
|
|
108
53
|
export function createAnalysisProvider(options) {
|
|
109
54
|
const fetchFn = options.fetchFn ?? undiciFetch;
|
|
55
|
+
const wire = WIRES[options.vendor ?? "openai"];
|
|
110
56
|
return async (request) => {
|
|
111
57
|
const failure = (errorCode, dispatched, status = "failed") => ({
|
|
112
58
|
status,
|
|
@@ -130,33 +76,7 @@ export function createAnalysisProvider(options) {
|
|
|
130
76
|
request.images.some((image) => !INPUT_IMAGE_DATA_URL.test(image.dataUrl))) {
|
|
131
77
|
return failure("invalid_request", false);
|
|
132
78
|
}
|
|
133
|
-
const
|
|
134
|
-
{ type: "input_text", text: request.evidence },
|
|
135
|
-
...request.images.flatMap((image) => [
|
|
136
|
-
{ type: "input_text", text: JSON.stringify({ captureEvidenceId: image.evidenceId }) },
|
|
137
|
-
{ type: "input_image", image_url: image.dataUrl, detail: "high" },
|
|
138
|
-
]),
|
|
139
|
-
];
|
|
140
|
-
const body = JSON.stringify({
|
|
141
|
-
model: request.model,
|
|
142
|
-
instructions: request.instructions,
|
|
143
|
-
input: [{ role: "user", content }],
|
|
144
|
-
store: false,
|
|
145
|
-
tools: [],
|
|
146
|
-
tool_choice: "none",
|
|
147
|
-
truncation: "disabled",
|
|
148
|
-
service_tier: "default",
|
|
149
|
-
reasoning: { effort: "high" },
|
|
150
|
-
max_output_tokens: request.maxOutputTokens,
|
|
151
|
-
text: {
|
|
152
|
-
format: {
|
|
153
|
-
type: "json_schema",
|
|
154
|
-
name: "study_analysis",
|
|
155
|
-
strict: true,
|
|
156
|
-
schema: request.schema,
|
|
157
|
-
},
|
|
158
|
-
},
|
|
159
|
-
});
|
|
79
|
+
const body = JSON.stringify(wire.body(request));
|
|
160
80
|
if (Buffer.byteLength(body) > MAX_REQUEST_BYTES)
|
|
161
81
|
return failure("invalid_request", false);
|
|
162
82
|
const controller = new AbortController();
|
|
@@ -173,11 +93,11 @@ export function createAnalysisProvider(options) {
|
|
|
173
93
|
if (request.signal?.aborted)
|
|
174
94
|
return failure("cancelled", false, "cancelled");
|
|
175
95
|
dispatched = true;
|
|
176
|
-
const response = await fetchFn(
|
|
96
|
+
const response = await fetchFn(wire.url(request.model), {
|
|
177
97
|
method: "POST",
|
|
178
98
|
redirect: "error",
|
|
179
99
|
signal: controller.signal,
|
|
180
|
-
headers:
|
|
100
|
+
headers: wire.headers(options.apiKey),
|
|
181
101
|
body,
|
|
182
102
|
dispatcher,
|
|
183
103
|
timeout: false,
|
|
@@ -211,7 +131,7 @@ export function createAnalysisProvider(options) {
|
|
|
211
131
|
if (controller.signal.aborted)
|
|
212
132
|
return failure(timedOut ? "timeout" : "cancelled", true, timedOut ? "timed_out" : "cancelled");
|
|
213
133
|
try {
|
|
214
|
-
return
|
|
134
|
+
return wire.parse(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
215
135
|
}
|
|
216
136
|
catch {
|
|
217
137
|
return failure("invalid_response", true);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type RunStatusRecord } from "../run/status.js";
|
|
2
|
+
/** What the gate reads: the run's status record, when it has one, and its streams. */
|
|
3
|
+
export interface RecordingFacts {
|
|
4
|
+
runId: string;
|
|
5
|
+
status: RunStatusRecord | undefined;
|
|
6
|
+
/** How many of the bundle's streams are still running or still hold live items. */
|
|
7
|
+
runningStreams: number;
|
|
8
|
+
streams: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Throws the refusal for a recording that has not ended. A `finished` record, an `interrupted`
|
|
12
|
+
* one, and a stale `running` one whose process is gone from this host have ended; analysis then
|
|
13
|
+
* also needs every stream ended. A fresh `running` record is a live run, and a stale one whose pid
|
|
14
|
+
* is still in use cannot be told apart from a live run, so both are refused.
|
|
15
|
+
*/
|
|
16
|
+
export declare function assertRecordingEnded(facts: RecordingFacts, nowMs?: number, processRunning?: (pid: number) => boolean): void;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Whether a run's recording has ended, so analysis can read it: the completion gate's reading of
|
|
2
|
+
// status.json and the bundle's streams. A refusal says what the run is doing and the command that
|
|
3
|
+
// moves it on, since "wait" helps only a run that is still alive.
|
|
4
|
+
import { cli } from "../cli/invocation.js";
|
|
5
|
+
import { classifyRunStatus, runOwnerGone } from "../run/status.js";
|
|
6
|
+
import { studyProvenanceOf } from "../run/study-provenance.js";
|
|
7
|
+
import { AnalysisSourceRefusedError } from "./source.js";
|
|
8
|
+
/**
|
|
9
|
+
* Throws the refusal for a recording that has not ended. A `finished` record, an `interrupted`
|
|
10
|
+
* one, and a stale `running` one whose process is gone from this host have ended; analysis then
|
|
11
|
+
* also needs every stream ended. A fresh `running` record is a live run, and a stale one whose pid
|
|
12
|
+
* is still in use cannot be told apart from a live run, so both are refused.
|
|
13
|
+
*/
|
|
14
|
+
export function assertRecordingEnded(facts, nowMs = Date.now(), processRunning) {
|
|
15
|
+
const { runId, status } = facts;
|
|
16
|
+
const runs = `\`${cli("runs")}\``;
|
|
17
|
+
if (status?.state === "running") {
|
|
18
|
+
if (classifyRunStatus(status, nowMs) === "running")
|
|
19
|
+
throw new AnalysisSourceRefusedError("ANALYSIS_RUN_ACTIVE", `Participants are still running in this run, so its recording has not ended. Automatic analysis starts when they finish; ${runs} shows when the run is analyzing.`);
|
|
20
|
+
if (!runOwnerGone(status, nowMs, processRunning))
|
|
21
|
+
throw new AnalysisSourceRefusedError("ANALYSIS_RUN_UNCONFIRMED", `This run's process stopped updating status.json at ${status.updatedAt}, but pid ${status.pid} is still in use on this machine, so humanish cannot tell whether the run is still writing. If the run is gone, \`${cli(`reclaim --run ${runId}`)}\` stops its sandboxes and marks it interrupted; then analyze it again.`);
|
|
22
|
+
}
|
|
23
|
+
if (facts.runningStreams === 0)
|
|
24
|
+
return;
|
|
25
|
+
if (status?.state === "finished")
|
|
26
|
+
throw new AnalysisSourceRefusedError("ANALYSIS_RUN_ACTIVE", `${facts.runningStreams} of ${facts.streams} participant streams in this run have not ended. ${runs} shows the run's state.`);
|
|
27
|
+
const study = status === undefined ? undefined : studyProvenanceOf(status)?.id;
|
|
28
|
+
throw new AnalysisSourceRefusedError("ANALYSIS_RUN_INCOMPLETE", `This run ended while ${facts.runningStreams} of ${facts.streams} participant streams were still running, so its recording is incomplete and is not analyzed. Run the study again with \`${cli(`run ${study ?? "<study>"}`)}\`.`);
|
|
29
|
+
}
|
|
@@ -15,5 +15,8 @@ export declare function checkProviderAnalysis(input: AnalysisInput, value: unkno
|
|
|
15
15
|
* each response as its own request, since the long-context tier re-prices one request at a time.
|
|
16
16
|
*/
|
|
17
17
|
export declare function recordProviderUsage(artifact: AnalysisArtifact, config: AnalysisConfig, responses: readonly AnalysisProviderResult[]): void;
|
|
18
|
+
/** The HTTP status an API analyst answered, for the attempt's warnings. The status code is the only
|
|
19
|
+
* part of the answer kept: the provider's error text may echo evidence or credentials. */
|
|
20
|
+
export declare function httpStatusWarning(config: AnalysisConfig, response: AnalysisProviderResult): string | undefined;
|
|
18
21
|
/** The warnings a Codex response carries about its CLI release and protocol. */
|
|
19
22
|
export declare function codexWarnings(config: AnalysisConfig, response: AnalysisProviderResult): string[];
|
|
@@ -185,6 +185,25 @@ export function recordProviderUsage(artifact, config, responses) {
|
|
|
185
185
|
usage.estimatedCostUsd = priced.estimatedCostUsd;
|
|
186
186
|
usage.ratesAsOf = priced.ratesAsOf;
|
|
187
187
|
}
|
|
188
|
+
/** What an HTTP status from an analysis API most likely means, by status. */
|
|
189
|
+
const HTTP_STATUS_MEANINGS = {
|
|
190
|
+
400: "it refused the request as invalid",
|
|
191
|
+
401: "it refused the key",
|
|
192
|
+
403: "the key has no access to this model",
|
|
193
|
+
404: "it does not know this model",
|
|
194
|
+
429: "a rate or quota limit of the key's plan was reached",
|
|
195
|
+
500: "a server error; a later attempt can succeed",
|
|
196
|
+
503: "the model is overloaded or unavailable; a later attempt can succeed",
|
|
197
|
+
};
|
|
198
|
+
/** The HTTP status an API analyst answered, for the attempt's warnings. The status code is the only
|
|
199
|
+
* part of the answer kept: the provider's error text may echo evidence or credentials. */
|
|
200
|
+
export function httpStatusWarning(config, response) {
|
|
201
|
+
if (response.httpStatus === undefined)
|
|
202
|
+
return undefined;
|
|
203
|
+
const api = config.provider === "gemini" ? "Gemini" : "OpenAI";
|
|
204
|
+
const meaning = HTTP_STATUS_MEANINGS[response.httpStatus];
|
|
205
|
+
return `The ${api} API answered HTTP ${response.httpStatus}${meaning === undefined ? "" : ` (${meaning})`}. humanish does not retry an analysis request; run the command again to retry.`;
|
|
206
|
+
}
|
|
188
207
|
/** The warnings a Codex response carries about its CLI release and protocol. */
|
|
189
208
|
export function codexWarnings(config, response) {
|
|
190
209
|
if (config.provider !== "codex")
|
|
@@ -58,9 +58,7 @@ export declare function resolveAnalysisRun(cwd: string, run: string, expectedRun
|
|
|
58
58
|
export declare function dryRunBundleRefusal(cwd: string, run: string, dryRun: boolean, expectedRun?: PreparedRunArtifactPaths): Promise<AnalyzeResult | null>;
|
|
59
59
|
/** Re-render the run's Observer after an analysis write. False when it could not be refreshed. */
|
|
60
60
|
export declare function refreshObserver(cwd: string, runId: string, prepared: PreparedRunArtifactPaths): Promise<boolean>;
|
|
61
|
-
/**
|
|
62
|
-
export declare function withAnalysisLock<T>(prepared: PreparedRunArtifactPaths, action: () => Promise<T>): Promise<T>;
|
|
63
|
-
/** The completion gate: the run verifies, is live and finished, and has participants. */
|
|
61
|
+
/** The completion gate: the run verifies, is live, its recording ended, and it has participants. */
|
|
64
62
|
export declare function readCompletedAnalysisSource(cwd: string, prepared: PreparedRunArtifactPaths): Promise<Buffer>;
|
|
65
63
|
export declare function analyzeRun(cwdInput: string, run: string, options: AnalyzeOptions, deps?: AnalyzeDeps): Promise<AnalyzeResult>;
|
|
66
64
|
export declare function showAnalysis(cwd: string, run: string, id?: string): Promise<LoadedAnalysis>;
|
package/dist/analysis/service.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { defaultCodexCliVersion, describeCodexCliAdmission, } from "../actors/codex/codex-admission.js";
|
|
2
2
|
import { validCodexAnalysisConfig } from "./codex-config.js";
|
|
3
|
+
import { analysisApiKey, analysisKeyNames, missingKeyHint } from "../keys/key-resolution.js";
|
|
3
4
|
import { randomUUID } from "node:crypto";
|
|
4
|
-
import {
|
|
5
|
+
import { realpath } from "node:fs/promises";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
import { renderObserver } from "../observer/render.js";
|
|
7
8
|
import { containsSensitive } from "../evidence/redaction.js";
|
|
@@ -11,6 +12,8 @@ import { verifyRunPrepared } from "../verify/verify.js";
|
|
|
11
12
|
import { loadRunBundlePrepared, resolveRunPath } from "../run/locate.js";
|
|
12
13
|
import { physicalCwdOf, runIdOf, resolvePhysicalCwd, validatePreparedRunRootIdentity, } from "../run/paths.js";
|
|
13
14
|
import { isRunStatusRecord, RUN_STATUS_FILE } from "../run/status.js";
|
|
15
|
+
import { assertRecordingEnded } from "./recording-ended.js";
|
|
16
|
+
import { withAnalysisLock } from "./analysis-lock.js";
|
|
14
17
|
import { captureEvidence, EVIDENCE_LIMITS } from "./evidence.js";
|
|
15
18
|
import { AnalysisSourceRefusedError, runFileTooLarge, throwAsAnalysisRefusal } from "./source.js";
|
|
16
19
|
import { pathMissing, readBoundedFile, readBoundedFileResult } from "../run/evidence-files.js";
|
|
@@ -46,14 +49,14 @@ export async function resolveAnalysisRun(cwd, run, expectedRun) {
|
|
|
46
49
|
const messages = {
|
|
47
50
|
ANALYSIS_RUN_NOT_FOUND: "The selected run is unavailable or its storage is unsafe.",
|
|
48
51
|
ANALYSIS_VERIFY_FAILED: "The run must pass verification before analysis.",
|
|
49
|
-
ANALYSIS_RUN_ACTIVE: "
|
|
52
|
+
ANALYSIS_RUN_ACTIVE: "This run's status.json is missing, unreadable or names another run, so humanish cannot tell whether its recording has ended. `humanish verify` checks the run's files.",
|
|
50
53
|
ANALYSIS_NO_PARTICIPANTS: "This run contains no participant evidence to analyze.",
|
|
51
54
|
ANALYSIS_REQUIRES_LIVE_RUN: "A dry run has no participant behavior to analyze. Select a completed live run.",
|
|
52
55
|
ANALYSIS_SOURCE_UNAVAILABLE: "The source recording is missing or unsafe.",
|
|
53
56
|
ANALYSIS_SOURCE_CHANGED: "The source recording changed during analysis. The result cannot be published against different evidence.",
|
|
54
|
-
ANALYSIS_BUSY: "Another analysis holds this run's .analysis-lock directory
|
|
57
|
+
ANALYSIS_BUSY: "Another analysis holds this run's .analysis-lock directory, and its process is still running or humanish cannot check it (another host, or a lock written by humanish 0.139.1 or earlier). Wait for it to finish; if no humanish process is running for this run, remove that directory.",
|
|
55
58
|
ANALYSIS_HISTORY_UNAVAILABLE: "Analysis history is unavailable or full. No request was sent. Inspect this run's analysis and analysis-attempts storage before retrying.",
|
|
56
|
-
ANALYSIS_CONFIG_INVALID: "OpenAI analysis
|
|
59
|
+
ANALYSIS_CONFIG_INVALID: "OpenAI and Gemini analysis need a supported model of that provider, a positive USD admission limit and 256–32768 output tokens. Codex account analysis uses its qualified model and null dollar/output-token caps. All require timeout 1–600000 ms.",
|
|
57
60
|
ANALYSIS_QUESTION_UNSAFE: "The reviewer question matched a sensitive-text pattern. Remove sensitive details before retrying.",
|
|
58
61
|
ANALYSIS_API_KEY_MISSING: "Set OPENAI_API_KEY to run analysis. --dry-run checks admission without a key or provider request.",
|
|
59
62
|
ANALYSIS_CANCELLED: "Analysis was cancelled before dispatch.",
|
|
@@ -109,43 +112,7 @@ export async function refreshObserver(cwd, runId, prepared) {
|
|
|
109
112
|
return false;
|
|
110
113
|
}
|
|
111
114
|
}
|
|
112
|
-
/**
|
|
113
|
-
export async function withAnalysisLock(prepared, action) {
|
|
114
|
-
await validatePreparedRunRootIdentity(prepared);
|
|
115
|
-
const target = path.join(prepared.physicalRunRoot, ".analysis-lock");
|
|
116
|
-
try {
|
|
117
|
-
await mkdir(target, { mode: 0o700 });
|
|
118
|
-
}
|
|
119
|
-
catch (error) {
|
|
120
|
-
if (error.code === "EEXIST")
|
|
121
|
-
throw new Error("ANALYSIS_BUSY");
|
|
122
|
-
throw new Error("ANALYSIS_UNAVAILABLE");
|
|
123
|
-
}
|
|
124
|
-
const identity = await lstat(target, { bigint: true });
|
|
125
|
-
try {
|
|
126
|
-
await validatePreparedRunRootIdentity(prepared);
|
|
127
|
-
if (!identity.isDirectory() || identity.isSymbolicLink())
|
|
128
|
-
throw new Error("ANALYSIS_UNAVAILABLE");
|
|
129
|
-
return await action();
|
|
130
|
-
}
|
|
131
|
-
finally {
|
|
132
|
-
// Only remove our empty, unchanged directory. No recursive deletion and no replacement authority.
|
|
133
|
-
try {
|
|
134
|
-
await validatePreparedRunRootIdentity(prepared);
|
|
135
|
-
const current = await lstat(target, { bigint: true });
|
|
136
|
-
if (current.isDirectory() &&
|
|
137
|
-
!current.isSymbolicLink() &&
|
|
138
|
-
current.dev === identity.dev &&
|
|
139
|
-
current.ino === identity.ino &&
|
|
140
|
-
current.birthtimeNs === identity.birthtimeNs)
|
|
141
|
-
await rmdir(target);
|
|
142
|
-
}
|
|
143
|
-
catch {
|
|
144
|
-
/* Leave ambiguous storage in place for explicit operator inspection. */
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
/** The completion gate: the run verifies, is live and finished, and has participants. */
|
|
115
|
+
/** The completion gate: the run verifies, is live, its recording ended, and it has participants. */
|
|
149
116
|
export async function readCompletedAnalysisSource(cwd, prepared) {
|
|
150
117
|
const read = await readBoundedFileResult(prepared, RUN_BUNDLE_FILE, EVIDENCE_LIMITS.sourceBytes);
|
|
151
118
|
if (read.state === "limit")
|
|
@@ -170,18 +137,19 @@ export async function readCompletedAnalysisSource(cwd, prepared) {
|
|
|
170
137
|
if (!missing)
|
|
171
138
|
throw new Error("ANALYSIS_RUN_ACTIVE");
|
|
172
139
|
}
|
|
140
|
+
let status;
|
|
173
141
|
if (statusBytes) {
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
if (!isRunStatusRecord(status) ||
|
|
177
|
-
status.runId !== loaded.bundle.runId ||
|
|
178
|
-
status.state !== "finished") {
|
|
142
|
+
const record = JSON.parse(statusBytes.toString("utf8"));
|
|
143
|
+
if (!isRunStatusRecord(record) || record.runId !== loaded.bundle.runId)
|
|
179
144
|
throw new Error("ANALYSIS_RUN_ACTIVE");
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
if (loaded.bundle.streams.some((stream) => !TERMINAL_SIMULATION_STATUSES.has(stream.status) || stream.liveActor !== undefined)) {
|
|
183
|
-
throw new Error("ANALYSIS_RUN_ACTIVE");
|
|
145
|
+
status = record;
|
|
184
146
|
}
|
|
147
|
+
assertRecordingEnded({
|
|
148
|
+
runId: loaded.bundle.runId,
|
|
149
|
+
status,
|
|
150
|
+
runningStreams: loaded.bundle.streams.filter((stream) => !TERMINAL_SIMULATION_STATUSES.has(stream.status) || stream.liveActor !== undefined).length,
|
|
151
|
+
streams: loaded.bundle.streams.length,
|
|
152
|
+
});
|
|
185
153
|
return bytes;
|
|
186
154
|
}
|
|
187
155
|
/** The refusal code for a configuration analyzeRun cannot run, before any run file is read. */
|
|
@@ -318,9 +286,16 @@ async function executeAnalysis(attempt) {
|
|
|
318
286
|
// An unreadable inventory is not evidence of an absent prior result.
|
|
319
287
|
// Check readable history capacity before any new paid attempt.
|
|
320
288
|
await assertAnalysisPublicationCapacity(prepared);
|
|
321
|
-
const apiKey = config.provider === "codex"
|
|
289
|
+
const apiKey = config.provider === "codex"
|
|
290
|
+
? ""
|
|
291
|
+
: (deps.apiKey ?? analysisApiKey(config.provider ?? "openai", process.env));
|
|
322
292
|
if (config.provider !== "codex" && !apiKey.trim())
|
|
323
|
-
return {
|
|
293
|
+
return {
|
|
294
|
+
...fail(input.runId, false, "ANALYSIS_API_KEY_MISSING", config.provider === "gemini"
|
|
295
|
+
? `Gemini analysis needs ${analysisKeyNames("gemini").join(" or ")}: ${missingKeyHint(analysisKeyNames("gemini")[0])}. --dry-run checks admission without a key or provider request.`
|
|
296
|
+
: undefined),
|
|
297
|
+
admission,
|
|
298
|
+
};
|
|
324
299
|
let finalizeExecution;
|
|
325
300
|
const rejection = {};
|
|
326
301
|
const analysis = await runAnalysis(input, config, {
|
|
@@ -384,7 +359,13 @@ export async function analyzeRun(cwdInput, run, options, deps = {}) {
|
|
|
384
359
|
if (deps.expectedRun !== undefined)
|
|
385
360
|
cwd = physicalCwdOf(prepared);
|
|
386
361
|
const execute = () => executeAnalysis({ cwd, run, prepared, config, options, deps, dryRun });
|
|
387
|
-
|
|
362
|
+
if (dryRun)
|
|
363
|
+
return await execute();
|
|
364
|
+
const tookOver = [];
|
|
365
|
+
const result = await withAnalysisLock(prepared, execute, (warning) => tookOver.push(warning));
|
|
366
|
+
return tookOver.length === 0
|
|
367
|
+
? result
|
|
368
|
+
: { ...result, warnings: [...tookOver, ...result.warnings] };
|
|
388
369
|
}
|
|
389
370
|
catch (error) {
|
|
390
371
|
if (error instanceof AnalysisSourceRefusedError)
|
|
@@ -7,7 +7,7 @@ import type { RunStream, StoredRunStream } from "../run/streams.js";
|
|
|
7
7
|
*/
|
|
8
8
|
export declare class AnalysisSourceRefusedError extends Error {
|
|
9
9
|
readonly reason: string;
|
|
10
|
-
constructor(code: "ANALYSIS_SOURCE_TOO_LARGE" | "ANALYSIS_SOURCE_INVALID", reason: string);
|
|
10
|
+
constructor(code: "ANALYSIS_SOURCE_TOO_LARGE" | "ANALYSIS_SOURCE_INVALID" | "ANALYSIS_RUN_ACTIVE" | "ANALYSIS_RUN_INCOMPLETE" | "ANALYSIS_RUN_UNCONFIRMED", reason: string);
|
|
11
11
|
}
|
|
12
12
|
/** The refusal of a run.json of `size` bytes, more than the `max` analysis reads of it. */
|
|
13
13
|
export declare function runFileTooLarge(size: number | bigint, max: number): AnalysisSourceRefusedError;
|
package/dist/analysis/types.d.ts
CHANGED
|
@@ -161,6 +161,10 @@ interface OpenAIAnalysisConfig {
|
|
|
161
161
|
timeoutMs: number;
|
|
162
162
|
maxOutputTokens: number;
|
|
163
163
|
}
|
|
164
|
+
/** The Gemini API analyst: the same billed, capped request as OpenAI's, sent to Google. */
|
|
165
|
+
interface GeminiAnalysisConfig extends Omit<OpenAIAnalysisConfig, "provider"> {
|
|
166
|
+
provider: "gemini";
|
|
167
|
+
}
|
|
164
168
|
/** Required, qualified execution profile, confirmed by the launcher before a turn.
|
|
165
169
|
* A failed pre-dispatch artifact does not prove this CLI/model was observed.
|
|
166
170
|
* These settings participate in cache and immutable attempt identity. */
|
|
@@ -184,7 +188,9 @@ export interface CodexAnalysisConfig {
|
|
|
184
188
|
identity: CodexAnalysisIdentity;
|
|
185
189
|
}
|
|
186
190
|
/** Omitted provider remains the historical OpenAI API contract, including its exact hash. */
|
|
187
|
-
export type AnalysisConfig = OpenAIAnalysisConfig | CodexAnalysisConfig;
|
|
191
|
+
export type AnalysisConfig = OpenAIAnalysisConfig | GeminiAnalysisConfig | CodexAnalysisConfig;
|
|
192
|
+
/** Who analyzed: an API billed by key (openai, gemini) or the restricted Codex account. */
|
|
193
|
+
export type AnalysisProviderName = "openai" | "gemini" | "codex";
|
|
188
194
|
export interface AnalysisUsage {
|
|
189
195
|
inputTokens: number | null;
|
|
190
196
|
outputTokens: number | null;
|
|
@@ -209,7 +215,7 @@ export interface AnalysisArtifact {
|
|
|
209
215
|
configDigest: string;
|
|
210
216
|
config: AnalysisConfig;
|
|
211
217
|
promptVersion: string;
|
|
212
|
-
provider:
|
|
218
|
+
provider: AnalysisProviderName;
|
|
213
219
|
usage: AnalysisUsage;
|
|
214
220
|
participants: AnalysisParticipantInput[];
|
|
215
221
|
coverage: AnalysisCoverage;
|
|
@@ -196,6 +196,15 @@ const analysisParticipantProvenanceSchema = z
|
|
|
196
196
|
.nullable(),
|
|
197
197
|
})
|
|
198
198
|
.strict();
|
|
199
|
+
/** The configuration of an analyst billed by API key. Stored artifacts keep the bounds of every
|
|
200
|
+
* release that wrote them; admission checks the current ones before a request. */
|
|
201
|
+
const apiAnalysisConfigSchema = z.object({
|
|
202
|
+
model: label,
|
|
203
|
+
question: text(4000).nullable(),
|
|
204
|
+
maxCostUsd: z.number().positive().max(1000),
|
|
205
|
+
timeoutMs: z.number().int().positive().max(3600000),
|
|
206
|
+
maxOutputTokens: z.number().int().positive().max(128000),
|
|
207
|
+
});
|
|
199
208
|
const analysisArtifactSchema = z
|
|
200
209
|
.object({
|
|
201
210
|
schema: z.literal(ANALYSIS_SCHEMA),
|
|
@@ -209,16 +218,8 @@ const analysisArtifactSchema = z
|
|
|
209
218
|
inputDigest: digest,
|
|
210
219
|
configDigest: digest,
|
|
211
220
|
config: z.union([
|
|
212
|
-
z
|
|
213
|
-
|
|
214
|
-
provider: z.literal("openai").optional(),
|
|
215
|
-
model: label,
|
|
216
|
-
question: text(4000).nullable(),
|
|
217
|
-
maxCostUsd: z.number().positive().max(1000),
|
|
218
|
-
timeoutMs: z.number().int().positive().max(3600000),
|
|
219
|
-
maxOutputTokens: z.number().int().positive().max(128000),
|
|
220
|
-
})
|
|
221
|
-
.strict(),
|
|
221
|
+
apiAnalysisConfigSchema.extend({ provider: z.literal("openai").optional() }).strict(),
|
|
222
|
+
apiAnalysisConfigSchema.extend({ provider: z.literal("gemini") }).strict(),
|
|
222
223
|
z
|
|
223
224
|
.object({
|
|
224
225
|
provider: z.literal("codex"),
|
|
@@ -243,7 +244,7 @@ const analysisArtifactSchema = z
|
|
|
243
244
|
.strict(),
|
|
244
245
|
]),
|
|
245
246
|
promptVersion: label,
|
|
246
|
-
provider: z.enum(["openai", "codex"]),
|
|
247
|
+
provider: z.enum(["openai", "gemini", "codex"]),
|
|
247
248
|
usage: z
|
|
248
249
|
.object({
|
|
249
250
|
cacheWriteInputTokens: z.number().int().min(0).max(1e12).nullable(),
|
|
@@ -547,7 +548,7 @@ export function validateAnalysisExecutionReceipt(value) {
|
|
|
547
548
|
const parsed = analysisExecutionReceiptSchema.safeParse(value);
|
|
548
549
|
if (!parsed.success || Date.parse(parsed.data.completedAt) < Date.parse(parsed.data.createdAt))
|
|
549
550
|
throw new Error("ANALYSIS_RECEIPT_INVALID");
|
|
550
|
-
if ((parsed.data.provider
|
|
551
|
+
if ((parsed.data.provider !== "codex" && parsed.data.maxCostUsd === null) ||
|
|
551
552
|
(parsed.data.provider === "codex" &&
|
|
552
553
|
(parsed.data.maxCostUsd !== null ||
|
|
553
554
|
parsed.data.usage.estimatedCostUsd !== null ||
|
|
@@ -287,6 +287,10 @@ declare const observationSchema: z.ZodObject<{
|
|
|
287
287
|
text: z.ZodString;
|
|
288
288
|
durationMs: z.ZodNumber;
|
|
289
289
|
}, z.core.$strict>>>;
|
|
290
|
+
externalHandoffs: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
291
|
+
scheme: z.ZodString;
|
|
292
|
+
recipients: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
293
|
+
}, z.core.$strict>>>;
|
|
290
294
|
}, z.core.$strict>;
|
|
291
295
|
type WireObservation = z.infer<typeof observationSchema>;
|
|
292
296
|
declare const requestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
@@ -503,6 +507,10 @@ declare const replySchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
503
507
|
text: z.ZodString;
|
|
504
508
|
durationMs: z.ZodNumber;
|
|
505
509
|
}, z.core.$strict>>>;
|
|
510
|
+
externalHandoffs: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
511
|
+
scheme: z.ZodString;
|
|
512
|
+
recipients: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
513
|
+
}, z.core.$strict>>>;
|
|
506
514
|
}, z.core.$strict>>;
|
|
507
515
|
recording: z.ZodOptional<z.ZodObject<{
|
|
508
516
|
mimeType: z.ZodLiteral<"video/mp4">;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PNG } from "pngjs";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { CUA_SPEECH_LIMITS } from "../actors/computer-use/speech.js";
|
|
4
|
+
import { EXTERNAL_HANDOFF_LIMITS } from "../actors/computer-use/external-handoff.js";
|
|
4
5
|
import { ComputerUseExecutorError, CUA_REJECTION_REASONS, isComputerUseExecutorError, } from "../actors/computer-use/executor-error.js";
|
|
5
6
|
import { CUA_DIAGNOSTIC_CATEGORIES, CUA_DIAGNOSTIC_STEPS, } from "../actors/computer-use/executor-diagnostic.js";
|
|
6
7
|
import { desktopRecordingMetadataSchema } from "../evidence/desktop-recording-types.js";
|
|
@@ -152,6 +153,17 @@ const heardSpeechListSchema = z
|
|
|
152
153
|
.min(1)
|
|
153
154
|
.max(BROWSER_CONTROL_LIMITS.heardSpeech)
|
|
154
155
|
.refine((rows) => new Set(rows.map((row) => row.id)).size === rows.length);
|
|
156
|
+
// The guest sends a handoff as externalHandoffOf made it: a scheme and a mailto link's recipients.
|
|
157
|
+
const externalHandoffListSchema = z
|
|
158
|
+
.array(z.strictObject({
|
|
159
|
+
scheme: z.string().regex(/^[a-z][a-z0-9+.-]{0,31}$/),
|
|
160
|
+
recipients: z
|
|
161
|
+
.array(z.string().min(3).max(EXTERNAL_HANDOFF_LIMITS.addressLength))
|
|
162
|
+
.max(EXTERNAL_HANDOFF_LIMITS.recipients)
|
|
163
|
+
.optional(),
|
|
164
|
+
}))
|
|
165
|
+
.min(1)
|
|
166
|
+
.max(EXTERNAL_HANDOFF_LIMITS.perObservation);
|
|
155
167
|
const observationSchema = z.strictObject({
|
|
156
168
|
png: z
|
|
157
169
|
.string()
|
|
@@ -165,6 +177,7 @@ const observationSchema = z.strictObject({
|
|
|
165
177
|
text: text.optional(),
|
|
166
178
|
scrollY: coordinate.optional(),
|
|
167
179
|
heardSpeech: heardSpeechListSchema.optional(),
|
|
180
|
+
externalHandoffs: externalHandoffListSchema.optional(),
|
|
168
181
|
});
|
|
169
182
|
const common = {
|
|
170
183
|
version: z.literal(BROWSER_CONTROL_VERSION),
|
|
@@ -344,6 +357,9 @@ export function encodeBrowserControlObservation(observation) {
|
|
|
344
357
|
...(observation.text !== undefined ? { text: observation.text } : {}),
|
|
345
358
|
...(observation.scrollY !== undefined ? { scrollY: observation.scrollY } : {}),
|
|
346
359
|
...(observation.heardSpeech !== undefined ? { heardSpeech: observation.heardSpeech } : {}),
|
|
360
|
+
...(observation.externalHandoffs !== undefined
|
|
361
|
+
? { externalHandoffs: observation.externalHandoffs }
|
|
362
|
+
: {}),
|
|
347
363
|
});
|
|
348
364
|
if (!parsed.success)
|
|
349
365
|
throw new ComputerUseExecutorError("invalid_response", "outcome_uncertain");
|
|
@@ -366,6 +382,11 @@ export function decodeBrowserControlObservation(value) {
|
|
|
366
382
|
...(state.text !== undefined ? { text: state.text } : {}),
|
|
367
383
|
...(state.scrollY !== undefined ? { scrollY: state.scrollY } : {}),
|
|
368
384
|
...(state.heardSpeech !== undefined ? { heardSpeech: state.heardSpeech } : {}),
|
|
385
|
+
...(state.externalHandoffs !== undefined
|
|
386
|
+
? {
|
|
387
|
+
externalHandoffs: state.externalHandoffs.map(({ scheme, recipients }) => recipients === undefined ? { scheme } : { scheme, recipients }),
|
|
388
|
+
}
|
|
389
|
+
: {}),
|
|
369
390
|
};
|
|
370
391
|
}
|
|
371
392
|
export function safeBrowserControlFailure(error, dispatched) {
|