humanish 0.123.0 → 0.125.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 +28 -0
- package/CONTEXT.md +2 -2
- package/README.md +2 -2
- package/dist/actors/computer-use/loop/trace.js +6 -1
- package/dist/actors/computer-use/openai-conversation.d.ts +0 -6
- package/dist/actors/computer-use/openai-conversation.js +0 -8
- package/dist/actors/computer-use/openai-provider.js +5 -8
- package/dist/analysis/evidence.js +2 -2
- package/dist/evidence/image.js +82 -3
- package/dist/feedback/export-bundle.js +1 -5
- package/dist/feedback/export.js +30 -11
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/observer/artifact.js +2 -9
- package/dist/observer/data.d.ts +9 -2
- package/dist/observer/data.js +17 -2
- package/dist/observer/page-data.d.ts +5 -0
- package/dist/observer/page-data.js +21 -0
- package/dist/observer/participant-items.d.ts +45 -0
- package/dist/observer/participant-items.js +85 -0
- package/dist/observer/pinned-files.d.ts +3 -1
- package/dist/observer/pinned-files.js +10 -7
- package/dist/observer/render.js +2 -1
- package/dist/observer/run-routes.js +16 -5
- package/dist/observer-app.html +4 -4
- package/dist/routes/computer-use/live-flush.d.ts +9 -4
- package/dist/routes/computer-use/live-flush.js +16 -3
- package/dist/run/bundle-shape.js +2 -2
- package/dist/run/bundle.d.ts +10 -3
- package/dist/run/bundle.js +11 -2
- package/dist/run/contained-output.d.ts +22 -0
- package/dist/run/contained-output.js +97 -1
- package/dist/run/evidence-files.d.ts +12 -1
- package/dist/run/evidence-files.js +70 -40
- package/dist/run/live-items.d.ts +48 -0
- package/dist/run/live-items.js +242 -0
- package/dist/run/locate.d.ts +10 -0
- package/dist/run/locate.js +10 -0
- package/dist/run/run.d.ts +3 -2
- package/dist/run/run.js +31 -9
- package/dist/run/sandbox-ids.d.ts +0 -5
- package/dist/run/sandbox-ids.js +0 -7
- package/dist/run/stream-trace.d.ts +18 -9
- package/dist/run/stream-trace.js +113 -22
- package/dist/run/streams.d.ts +17 -2
- package/dist/study/parse/actors.js +23 -6
- package/dist/study/parse/execution.js +6 -0
- package/dist/study/plan-base.js +4 -1
- package/dist/study/plan.js +20 -0
- package/dist/verify/artifacts.js +39 -51
- package/dist/verify/screenshot-reads.d.ts +21 -0
- package/dist/verify/screenshot-reads.js +46 -0
- package/dist/verify/verify.js +7 -6
- package/docs/architecture/github-feedback-loop.md +1 -1
- package/docs/architecture/observer.md +24 -5
- package/docs/contracts/README.md +1 -1
- package/docs/contracts/run-bundle.md +78 -14
- package/docs/contracts/schemas.md +38 -37
- package/docs/contracts/study-analysis.md +1 -1
- package/docs/decisions/0002-observer-is-one-self-contained-file.md +2 -1
- package/package.json +4 -2
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// The Observer's data in two parts: observer-data.json, the run and every participant without
|
|
2
|
+
// their trace items, and the items themselves. A participant working for hours records thousands
|
|
3
|
+
// of items, so the items are most of the bytes; apart, the saved observer-data.json stays small
|
|
4
|
+
// enough to verify, and the page carries the items compressed. The Observer bundles this module,
|
|
5
|
+
// so it imports types only (docs/decisions/0002-observer-is-one-self-contained-file.md).
|
|
6
|
+
export const OBSERVER_ITEMS_SCHEMA = "humanish.observer-items.v1";
|
|
7
|
+
/** The data as observer-data.json and the page's items carry it. */
|
|
8
|
+
export function splitObserverData(data) {
|
|
9
|
+
// No prototype, so a stream id such as `__proto__` is an entry like any other.
|
|
10
|
+
const items = Object.create(null);
|
|
11
|
+
const streams = data.streams.map((stream) => {
|
|
12
|
+
const { actor, liveActor, ...rest } = stream;
|
|
13
|
+
const entry = {};
|
|
14
|
+
const shell = rest;
|
|
15
|
+
// Saved projections are read unchecked, and one written by an older version may have no list.
|
|
16
|
+
if (Array.isArray(stream.artifacts) &&
|
|
17
|
+
stream.artifacts.some((artifact) => artifact.kind === "screenshot")) {
|
|
18
|
+
entry.artifacts = stream.artifacts;
|
|
19
|
+
shell.artifacts = stream.artifacts.filter((artifact) => artifact.kind !== "screenshot");
|
|
20
|
+
shell.screenshotArtifacts = stream.artifacts.length - shell.artifacts.length;
|
|
21
|
+
}
|
|
22
|
+
if (actor !== undefined) {
|
|
23
|
+
const { items: actorItems, ...summary } = actor;
|
|
24
|
+
entry.actor = actorItems;
|
|
25
|
+
shell.actor = summary;
|
|
26
|
+
}
|
|
27
|
+
if (liveActor !== undefined) {
|
|
28
|
+
const { items: liveItems, ...summary } = liveActor;
|
|
29
|
+
entry.liveActor = liveItems;
|
|
30
|
+
shell.liveActor = summary;
|
|
31
|
+
}
|
|
32
|
+
if (Object.keys(entry).length > 0)
|
|
33
|
+
items[stream.id] = entry;
|
|
34
|
+
return shell;
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
shell: { ...data, streams },
|
|
38
|
+
items: { schema: OBSERVER_ITEMS_SCHEMA, runId: data.run.runId, streams: items },
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Whether `items` are this run's and hold every participant's items that the data leaves out. The
|
|
43
|
+
* page reads nothing short of that: participants shown with no steps would say they did nothing.
|
|
44
|
+
*/
|
|
45
|
+
export function itemsCoverShell(shell, items) {
|
|
46
|
+
if (items.runId !== shell.run.runId)
|
|
47
|
+
return false;
|
|
48
|
+
return shell.streams.every((stream) => {
|
|
49
|
+
const entry = Object.hasOwn(items.streams, stream.id) ? items.streams[stream.id] : undefined;
|
|
50
|
+
return ((stream.actor === undefined || Array.isArray(entry?.actor)) &&
|
|
51
|
+
(stream.liveActor === undefined || Array.isArray(entry?.liveActor)) &&
|
|
52
|
+
(stream.screenshotArtifacts === undefined ||
|
|
53
|
+
(Array.isArray(entry?.artifacts) &&
|
|
54
|
+
entry.artifacts.filter((artifact) => artifact.kind === "screenshot").length ===
|
|
55
|
+
stream.screenshotArtifacts)));
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/** Whether the data leaves out items or screenshot entries that only an items document holds. */
|
|
59
|
+
export function shellNeedsItems(shell) {
|
|
60
|
+
return shell.streams.some((stream) => stream.actor !== undefined ||
|
|
61
|
+
stream.liveActor !== undefined ||
|
|
62
|
+
stream.screenshotArtifacts !== undefined);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The data with each participant's items back in place. A stream the items do not cover keeps an
|
|
66
|
+
* empty list, so the page shows the participant with no recorded steps.
|
|
67
|
+
*/
|
|
68
|
+
export function joinObserverData(shell, items) {
|
|
69
|
+
const byStream = items !== null && items.runId === shell.run.runId ? items.streams : {};
|
|
70
|
+
return {
|
|
71
|
+
...shell,
|
|
72
|
+
streams: shell.streams.map((stream) => {
|
|
73
|
+
const entry = Object.hasOwn(byStream, stream.id) ? byStream[stream.id] : undefined;
|
|
74
|
+
const { actor, liveActor, screenshotArtifacts: _moved, ...rest } = stream;
|
|
75
|
+
return {
|
|
76
|
+
...rest,
|
|
77
|
+
artifacts: entry?.artifacts ?? stream.artifacts,
|
|
78
|
+
...(actor === undefined ? {} : { actor: { ...actor, items: entry?.actor ?? [] } }),
|
|
79
|
+
...(liveActor === undefined
|
|
80
|
+
? {}
|
|
81
|
+
: { liveActor: { ...liveActor, items: entry?.liveActor ?? [] } }),
|
|
82
|
+
};
|
|
83
|
+
}),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -17,10 +17,12 @@ export interface PinnedDirectory {
|
|
|
17
17
|
/**
|
|
18
18
|
* The file's bytes, or null when it cannot be read under the root's checks. With `maxBytes`, a
|
|
19
19
|
* file larger than that is refused from its opened size and the read stops there, so a large file
|
|
20
|
-
* is never held in memory.
|
|
20
|
+
* is never held in memory. With `length`, only the file's first `length` bytes are read, or all of
|
|
21
|
+
* a shorter file.
|
|
21
22
|
*/
|
|
22
23
|
export declare function readContainedFile(root: PinnedDirectory, filePathInput: string, options?: {
|
|
23
24
|
maxBytes?: number;
|
|
25
|
+
length?: number;
|
|
24
26
|
}): Promise<Buffer | null>;
|
|
25
27
|
export declare function openContainedFile(root: PinnedDirectory, filePathInput: string): Promise<{
|
|
26
28
|
handle: FileHandle;
|
|
@@ -5,23 +5,26 @@ import { createHash } from "node:crypto";
|
|
|
5
5
|
import { constants as fsConstants } from "node:fs";
|
|
6
6
|
import { lstat, open, realpath } from "node:fs/promises";
|
|
7
7
|
import path from "node:path";
|
|
8
|
-
import { readOpenedAtMost } from "../run/contained-output.js";
|
|
8
|
+
import { readOpenedAtMost, readOpenedPrefix } from "../run/contained-output.js";
|
|
9
9
|
import { isPathInside, isSafeRunIdSegment } from "../run/paths.js";
|
|
10
10
|
/**
|
|
11
11
|
* The file's bytes, or null when it cannot be read under the root's checks. With `maxBytes`, a
|
|
12
12
|
* file larger than that is refused from its opened size and the read stops there, so a large file
|
|
13
|
-
* is never held in memory.
|
|
13
|
+
* is never held in memory. With `length`, only the file's first `length` bytes are read, or all of
|
|
14
|
+
* a shorter file.
|
|
14
15
|
*/
|
|
15
16
|
export async function readContainedFile(root, filePathInput, options = {}) {
|
|
16
17
|
const opened = await openContainedFile(root, filePathInput);
|
|
17
18
|
if (!opened)
|
|
18
19
|
return null;
|
|
19
20
|
try {
|
|
20
|
-
const body = options.
|
|
21
|
-
? await opened.handle.
|
|
22
|
-
:
|
|
23
|
-
?
|
|
24
|
-
:
|
|
21
|
+
const body = options.length !== undefined
|
|
22
|
+
? await readOpenedPrefix(opened.handle, options.length)
|
|
23
|
+
: options.maxBytes === undefined
|
|
24
|
+
? await opened.handle.readFile()
|
|
25
|
+
: opened.size > options.maxBytes
|
|
26
|
+
? null
|
|
27
|
+
: await readOpenedAtMost(opened.handle, options.maxBytes);
|
|
25
28
|
if (body === null)
|
|
26
29
|
return null;
|
|
27
30
|
// A write that landed during the read changed the file's mtime and ctime.
|
package/dist/observer/render.js
CHANGED
|
@@ -17,6 +17,7 @@ import { RUN_STATUS_FILE } from "../run/status.js";
|
|
|
17
17
|
import { verifyRunPrepared } from "../verify/verify.js";
|
|
18
18
|
import { renderObserverHtml } from "./artifact.js";
|
|
19
19
|
import { buildObserverData } from "./data.js";
|
|
20
|
+
import { splitObserverData } from "./participant-items.js";
|
|
20
21
|
import { buildServeSecurityHeaders, hostAllowed, parsePublicOrigin } from "./http.js";
|
|
21
22
|
import { listenOnLoopback } from "./listen.js";
|
|
22
23
|
import { NOTES_PATH } from "../run/note-shape.js";
|
|
@@ -116,7 +117,7 @@ export async function renderObserver(cwdInput, runInput, options = {}) {
|
|
|
116
117
|
verifiedAt: new Date().toISOString(),
|
|
117
118
|
reasons: verified.shareSafety.reasons.map((reason) => reason.code),
|
|
118
119
|
};
|
|
119
|
-
await writeContainedOutputFile(preparedRunPaths, path.join("observer", "observer-data.json"), `${JSON.stringify(observerData, null, 2)}\n`, "utf8");
|
|
120
|
+
await writeContainedOutputFile(preparedRunPaths, path.join("observer", "observer-data.json"), `${JSON.stringify(splitObserverData(observerData).shell, null, 2)}\n`, "utf8");
|
|
120
121
|
await writeContainedOutputFile(preparedRunPaths, path.join("observer", "study-analysis.json"), `${JSON.stringify(analysis, null, 2)}\n`, "utf8");
|
|
121
122
|
await writeContainedOutputFile(preparedRunPaths, path.join("observer", "index.html"), renderObserverHtml(observerData, { analysis, notes: await readRunNotes(preparedRunPaths) }), "utf8");
|
|
122
123
|
await validatePreparedRunArtifactPaths(preparedRunPaths);
|
|
@@ -16,7 +16,8 @@ import { MAX_NOTE_FILE_BYTES, readRunNotes, RUN_NOTES_DIR, } from "../run/note-f
|
|
|
16
16
|
import { analysisCostOf, runCost, runCostLabel } from "../run/run-cost.js";
|
|
17
17
|
import { RUN_STATUS_FILE, RUN_STATUS_STALE_MS, isRunStatusRecord, } from "../run/status.js";
|
|
18
18
|
import { renderObserverHtml } from "./artifact.js";
|
|
19
|
-
import {
|
|
19
|
+
import { splitObserverData } from "./participant-items.js";
|
|
20
|
+
import { buildObserverData, observerDataFromSaved, recordedStreamEmbed, withObserverEndings, } from "./data.js";
|
|
20
21
|
import { buildArtifactSecurityHeaders } from "./http.js";
|
|
21
22
|
import { assertPinnedDirectory, openContainedFile, pinDirectChildDirectory, readContainedFile, relativeToRoot, sha256OfOpenedFile, } from "./pinned-files.js";
|
|
22
23
|
/** The run's artifact paths, only when they are still the pinned directory being served. */
|
|
@@ -116,13 +117,19 @@ export async function serveRunPath(runRoot, relativePath, response, runtimeStrea
|
|
|
116
117
|
}), "text/html; charset=utf-8");
|
|
117
118
|
return;
|
|
118
119
|
}
|
|
119
|
-
|
|
120
|
+
// observer-data.json is the run without its participants' items, as the run saves it. The page
|
|
121
|
+
// polls observer-feed.json, the same data with the items, so one projection serves each poll.
|
|
122
|
+
if (cleanedRelativePath === "observer/observer-data.json" ||
|
|
123
|
+
cleanedRelativePath === "observer/observer-feed.json") {
|
|
120
124
|
const observerData = await observerDataOrReason(runRoot, runtimeStreamUrls);
|
|
121
125
|
if ("unavailable" in observerData) {
|
|
122
126
|
writeResponse(response, 404, `Observer data not found: ${observerData.unavailable}`, "text/plain; charset=utf-8");
|
|
123
127
|
return;
|
|
124
128
|
}
|
|
125
|
-
|
|
129
|
+
const { shell, items } = splitObserverData(observerData);
|
|
130
|
+
writeResponse(response, 200, cleanedRelativePath === "observer/observer-data.json"
|
|
131
|
+
? JSON.stringify(shell, null, 2)
|
|
132
|
+
: JSON.stringify({ data: shell, items }), "application/json; charset=utf-8");
|
|
126
133
|
return;
|
|
127
134
|
}
|
|
128
135
|
if (cleanedRelativePath === "observer/study-analysis.json") {
|
|
@@ -158,9 +165,10 @@ export async function serveRunPath(runRoot, relativePath, response, runtimeStrea
|
|
|
158
165
|
}
|
|
159
166
|
/** The run's trace files, read through its pinned folder. */
|
|
160
167
|
function pinnedTraceReader(runRoot) {
|
|
161
|
-
return async (relativePath, maxBytes) => {
|
|
168
|
+
return async (relativePath, maxBytes, length) => {
|
|
162
169
|
const bytes = await readContainedFile(runRoot, path.join(runRoot.physicalPath, relativePath), {
|
|
163
170
|
maxBytes,
|
|
171
|
+
...(length === undefined ? {} : { length }),
|
|
164
172
|
}).catch(() => null);
|
|
165
173
|
return bytes === null ? { refused: `${relativePath} could not be read` } : { bytes };
|
|
166
174
|
};
|
|
@@ -214,7 +222,10 @@ async function savedObserverData(runRoot, record, runtimeStreamUrls) {
|
|
|
214
222
|
const observerBytes = await readContainedFile(runRoot, path.join(runRoot.physicalPath, "observer", "observer-data.json"));
|
|
215
223
|
if (!observerBytes)
|
|
216
224
|
throw new Error("observer-data.json unavailable");
|
|
217
|
-
|
|
225
|
+
const saved = observerDataFromSaved(JSON.parse(observerBytes.toString("utf8")));
|
|
226
|
+
if (saved === null)
|
|
227
|
+
throw new Error("observer-data.json holds no participant's items");
|
|
228
|
+
return withRuntimeStreamUrls(withLocalRunStatus(record, withObserverEndings(saved)), runtimeStreamUrls);
|
|
218
229
|
}
|
|
219
230
|
catch {
|
|
220
231
|
return { unavailable: "The run has written no run.json or Observer data yet." };
|