humanish 0.110.3 → 0.111.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 +29 -0
- package/CONTEXT.md +2 -2
- package/dist/actors/local-agent/claude-session.js +1 -1
- package/dist/actors/local-agent/cli.js +1 -1
- package/dist/actors/scripted-browser/actor.d.ts +2 -2
- package/dist/actors/scripted-browser/actor.js +17 -8
- package/dist/actors/scripted-browser/launch-env.d.ts +12 -0
- package/dist/actors/scripted-browser/launch-env.js +50 -0
- package/dist/analysis/automatic-completion.d.ts +2 -1
- package/dist/analysis/automatic-completion.js +7 -2
- package/dist/cli/commands/migrate.js +1 -1
- package/dist/cli/commands/runs.js +5 -2
- package/dist/cli/commands/study-route-computer-use.js +3 -2
- package/dist/cli/commands/study-route-preview.js +2 -1
- package/dist/cli/commands/study-route-shared-world.js +3 -3
- package/dist/cli/commands/study.js +2 -1
- package/dist/routes/computer-use/live-phase.js +8 -1
- package/dist/routes/computer-use/local-vm.js +2 -1
- package/dist/routes/computer-use/participant-model.js +1 -1
- package/dist/routes/computer-use/plan.js +9 -10
- package/dist/routes/computer-use/preflight.js +1 -1
- package/dist/routes/computer-use/result.js +1 -1
- package/dist/routes/computer-use/route.js +3 -2
- package/dist/routes/computer-use/setup.js +2 -1
- package/dist/routes/computer-use/types.d.ts +4 -4
- package/dist/routes/scripted/plan.js +5 -4
- package/dist/routes/scripted/route.js +6 -2
- package/dist/routes/scripted/scenario.d.ts +1 -1
- package/dist/routes/scripted/scenario.js +3 -3
- package/dist/routes/scripted/surface-sessions.d.ts +2 -0
- package/dist/routes/scripted/surface-sessions.js +2 -1
- package/dist/routes/scripted/types.d.ts +2 -2
- package/dist/routes/shared-world/handoff.js +1 -1
- package/dist/routes/shared-world/participant-specs.js +6 -2
- package/dist/routes/shared-world/plan.js +8 -8
- package/dist/routes/shared-world/route.js +3 -3
- package/dist/routes/shared-world/types.d.ts +1 -1
- package/dist/routes/terminal/bundle.d.ts +3 -3
- package/dist/routes/terminal/ledger.d.ts +2 -2
- package/dist/routes/terminal/lifetime.js +1 -1
- package/dist/routes/terminal/live-finish.js +1 -1
- package/dist/routes/terminal/plan.js +4 -3
- package/dist/routes/terminal/route.js +3 -2
- package/dist/routes/terminal/session.js +1 -0
- package/dist/routes/terminal/types.d.ts +1 -1
- package/dist/run/display.d.ts +10 -0
- package/dist/run/display.js +22 -0
- package/dist/run/reclaim.d.ts +4 -2
- package/dist/run/reclaim.js +61 -18
- package/dist/run/results.d.ts +1 -1
- package/dist/run/run.d.ts +11 -1
- package/dist/run/run.js +24 -5
- package/dist/run/status.d.ts +6 -0
- package/dist/run/status.js +1 -0
- package/dist/run/stored-runs.js +26 -11
- package/dist/run-study.d.ts +5 -3
- package/dist/run-study.js +7 -11
- package/dist/study/adapter-scorer-loader.d.ts +1 -1
- package/dist/study/adapter-scorer-loader.js +3 -3
- package/dist/study/composition-rules.js +49 -39
- package/dist/study/config.d.ts +31 -8
- package/dist/study/config.js +181 -155
- package/dist/study/discover.d.ts +1 -1
- package/dist/study/discover.js +1 -1
- package/dist/study/doctor.js +5 -4
- package/dist/study/keys.d.ts +250 -2
- package/dist/study/keys.js +32 -62
- package/dist/study/{convert.d.ts → migrate/convert.d.ts} +1 -1
- package/dist/study/{convert.js → migrate/convert.js} +28 -31
- package/dist/study/{migrate.d.ts → migrate/migrate.d.ts} +1 -1
- package/dist/study/{migrate.js → migrate/migrate.js} +2 -2
- package/dist/study/{owned-files.js → migrate/owned-files.js} +1 -1
- package/dist/study/migrate/v2.d.ts +78 -0
- package/dist/study/migrate/v2.js +607 -0
- package/dist/study/parse/actors.d.ts +34 -10
- package/dist/study/parse/actors.js +76 -174
- package/dist/study/parse/comms.js +6 -2
- package/dist/study/parse/execution.d.ts +9 -4
- package/dist/study/parse/execution.js +6 -41
- package/dist/study/parse/front.d.ts +36 -0
- package/dist/study/parse/{study-v3.js → front.js} +37 -88
- package/dist/study/parse/subject.d.ts +2 -0
- package/dist/study/parse/subject.js +15 -18
- package/dist/study/persona-resolve.d.ts +2 -4
- package/dist/study/persona-resolve.js +7 -8
- package/dist/study/plan-base.d.ts +1 -1
- package/dist/study/plan-base.js +6 -6
- package/dist/study/plan-participants.d.ts +2 -2
- package/dist/study/plan-participants.js +12 -11
- package/dist/study/plan-types.d.ts +5 -5
- package/dist/study/plan.js +5 -4
- package/dist/study/preflight-probes.js +4 -4
- package/dist/study/preflight.d.ts +1 -1
- package/dist/study/preflight.js +3 -3
- package/dist/study/routing.d.ts +5 -5
- package/dist/study/routing.js +17 -20
- package/dist/study/run-study-options.d.ts +1 -1
- package/dist/study/run-study-options.js +3 -3
- package/dist/study/study-deps.d.ts +1 -1
- package/dist/study/study-fields.d.ts +19 -0
- package/dist/study/study-fields.js +46 -0
- package/dist/study/summary.js +19 -18
- package/dist/study/types.d.ts +66 -104
- package/dist/study/types.js +0 -1
- package/dist/study/url-credentials.js +3 -3
- package/dist/study/validation.d.ts +4 -10
- package/dist/study/validation.js +54 -79
- package/dist/study/warnings.d.ts +3 -3
- package/dist/study/warnings.js +42 -110
- package/dist/substrates/local/runtime-config.js +14 -16
- package/dist/tui-app.js +3 -3
- package/docs/contracts/schemas.md +36 -8
- package/package.json +1 -1
- package/dist/study/parse/study-v3.d.ts +0 -21
- /package/dist/study/{owned-files.d.ts → migrate/owned-files.d.ts} +0 -0
|
@@ -4,24 +4,15 @@ import { isMaxOutputTokens } from "../../actors/output-token-limit.js";
|
|
|
4
4
|
import { isHttpUrl } from "./subject.js";
|
|
5
5
|
import { entryCredentialReason, urlCredentialReason } from "./url-credentials.js";
|
|
6
6
|
import { registeredComputerUseActors, registeredScriptedBrowserActors, registeredTerminalActors, } from "../routing.js";
|
|
7
|
-
import { invalid,
|
|
7
|
+
import { invalid, str } from "./values.js";
|
|
8
8
|
import { isRecord } from "../../run/type-guards.js";
|
|
9
9
|
// A participant id interpolates into its evidence paths (screenshots/<id>/, actors/<id>.json), so
|
|
10
10
|
// it must be a public-safe path token, same shape as a study id.
|
|
11
11
|
export const PARTICIPANT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
12
12
|
export const PARTICIPANT_ID_MAX_CHARS = 40;
|
|
13
13
|
const METADATA_MAX_CHARS = 80;
|
|
14
|
-
/** The participant entries an actor declares, after roster groups expand into them. The manifest
|
|
15
|
-
* spells them `actors[].lanes`. Undefined when the actor declares none. */
|
|
16
|
-
export function rosterOf(actor) {
|
|
17
|
-
return actor?.lanes;
|
|
18
|
-
}
|
|
19
|
-
/** An actor's per-participant focus on the app-url route. The manifest spells it `laneFocus`. */
|
|
20
|
-
export function focusOf(actor) {
|
|
21
|
-
return actor?.laneFocus;
|
|
22
|
-
}
|
|
23
14
|
// Actor ids humanish no longer registers. Rejecting them at parse keeps a study that names one
|
|
24
|
-
// from running on a route that ignores
|
|
15
|
+
// from running on a route that ignores actor.type, such as a this-repo dry run.
|
|
25
16
|
const REMOVED_ACTOR_TYPES = new Set(["pi-agent-core", "claude-agent-sdk"]);
|
|
26
17
|
// Only actors of a run kind a route dispatches. codex-app-server is registered but declares only
|
|
27
18
|
// the "code" run kind, which no study route runs, so naming it would lead to the same dead end.
|
|
@@ -32,187 +23,98 @@ function routableActorTypes() {
|
|
|
32
23
|
...registeredTerminalActors(),
|
|
33
24
|
];
|
|
34
25
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// silently ignore actors[1..]; multi-actor support lands in a later slice.
|
|
41
|
-
if (raw.length > 1) {
|
|
42
|
-
return invalid("Multiple actors are not supported yet (only the first actor runs); declare a single actor.");
|
|
43
|
-
}
|
|
44
|
-
const actors = [];
|
|
45
|
-
for (const [index, entry] of raw.entries()) {
|
|
46
|
-
if (!isRecord(entry)) {
|
|
47
|
-
return invalid(`actors[${index}] must be an object.`);
|
|
48
|
-
}
|
|
49
|
-
const type = str(entry.type);
|
|
50
|
-
if (!type) {
|
|
51
|
-
return invalid(`actors[${index}].type is required.`);
|
|
52
|
-
}
|
|
53
|
-
if (REMOVED_ACTOR_TYPES.has(type)) {
|
|
54
|
-
return invalid(`actors[${index}].type "${type}" is no longer a humanish actor. Use an actor a study route runs (one of: ${routableActorTypes().join(", ")}). To drive a study with a signed-in Claude Code, use type: local-agent with localAgent: claude.`);
|
|
55
|
-
}
|
|
56
|
-
const actor = { type };
|
|
57
|
-
const count = posInt(entry.count);
|
|
58
|
-
if (count !== undefined)
|
|
59
|
-
actor.count = count;
|
|
60
|
-
if (entry.lanes !== undefined && entry.roster !== undefined) {
|
|
61
|
-
return invalid(`Set either actors[${index}].lanes or actors[${index}].roster, not both: list participants one by one in \`lanes\`, or in groups in \`roster\`.`);
|
|
62
|
-
}
|
|
63
|
-
if (entry.roster !== undefined && count !== undefined) {
|
|
64
|
-
return invalid(`Set either actors[${index}].roster or actors[${index}].count, not both: \`roster\` declares groups of distinct participants, and \`count\` declares identical ones.`);
|
|
65
|
-
}
|
|
66
|
-
if (entry.roster !== undefined && entry.laneFocus !== undefined) {
|
|
67
|
-
return invalid(`actors[${index}].roster and actors[${index}].laneFocus are mutually exclusive: a roster group's instruction is each participant's steer.`);
|
|
68
|
-
}
|
|
69
|
-
const rosterResult = entry.roster !== undefined
|
|
70
|
-
? parseRosterGroups(entry.roster, index)
|
|
71
|
-
: parseParticipantEntries(entry.lanes, index);
|
|
72
|
-
if (!rosterResult.ok) {
|
|
73
|
-
return rosterResult;
|
|
74
|
-
}
|
|
75
|
-
if (rosterResult.value)
|
|
76
|
-
actor.lanes = rosterResult.value;
|
|
77
|
-
const persona = str(entry.persona);
|
|
78
|
-
if (persona)
|
|
79
|
-
actor.persona = persona;
|
|
80
|
-
const mission = str(entry.mission);
|
|
81
|
-
if (mission)
|
|
82
|
-
actor.mission = mission;
|
|
83
|
-
const model = str(entry.model);
|
|
84
|
-
if (model)
|
|
85
|
-
actor.model = model;
|
|
86
|
-
if (entry.maxOutputTokens !== undefined) {
|
|
87
|
-
if (!isMaxOutputTokens(entry.maxOutputTokens))
|
|
88
|
-
return invalid(`actors[${index}].maxOutputTokens must be a positive safe integer.`);
|
|
89
|
-
actor.maxOutputTokens = entry.maxOutputTokens;
|
|
90
|
-
}
|
|
91
|
-
const localAgent = str(entry.localAgent);
|
|
92
|
-
if (entry.localAgent !== undefined) {
|
|
93
|
-
if (localAgent !== "codex" && localAgent !== "claude") {
|
|
94
|
-
return invalid(`actors[${index}].localAgent must be "codex" or "claude" (the locally signed-in CLI that drives the study).`);
|
|
95
|
-
}
|
|
96
|
-
actor.localAgent = localAgent;
|
|
97
|
-
}
|
|
98
|
-
if (entry.reasoningEffort !== undefined) {
|
|
99
|
-
if (!isReasoningEffort(entry.reasoningEffort)) {
|
|
100
|
-
return invalid(`actors[${index}].reasoningEffort must be one of: ${reasoningEffortNames()}. Support is model-dependent, so a level this model does not accept fails on the first turn rather than being silently downgraded.`);
|
|
101
|
-
}
|
|
102
|
-
actor.reasoningEffort = entry.reasoningEffort;
|
|
103
|
-
}
|
|
104
|
-
const stopWhenResult = parseStopWhen(entry.stopWhen, `actors[${index}].stopWhen`);
|
|
105
|
-
if (!stopWhenResult.ok)
|
|
106
|
-
return stopWhenResult;
|
|
107
|
-
if (stopWhenResult.value !== undefined)
|
|
108
|
-
actor.stopWhen = stopWhenResult.value;
|
|
109
|
-
const dwellResult = parseDwell(entry.dwell, `actors[${index}].dwell`);
|
|
110
|
-
if (!dwellResult.ok)
|
|
111
|
-
return dwellResult;
|
|
112
|
-
if (dwellResult.value !== undefined)
|
|
113
|
-
actor.dwell = dwellResult.value;
|
|
114
|
-
const tasksResult = parseTasks(entry.tasks, `actors[${index}].tasks`);
|
|
115
|
-
if (!tasksResult.ok)
|
|
116
|
-
return tasksResult;
|
|
117
|
-
if (tasksResult.value !== undefined)
|
|
118
|
-
actor.tasks = tasksResult.value;
|
|
119
|
-
const focus = parseFocus(entry.laneFocus);
|
|
120
|
-
if (focus)
|
|
121
|
-
actor.laneFocus = focus;
|
|
122
|
-
actors.push(actor);
|
|
26
|
+
/** An actor's `type`, which a study route must run. `path` names the actor in messages. */
|
|
27
|
+
export function parseActorType(entry, path) {
|
|
28
|
+
const type = str(entry.type);
|
|
29
|
+
if (!type) {
|
|
30
|
+
return invalid(`${path}.type is required.`);
|
|
123
31
|
}
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
function parseFocus(raw) {
|
|
127
|
-
if (!isRecord(raw)) {
|
|
128
|
-
return undefined;
|
|
32
|
+
if (REMOVED_ACTOR_TYPES.has(type)) {
|
|
33
|
+
return invalid(`${path}.type "${type}" is no longer a humanish actor. Use an actor a study route runs (one of: ${routableActorTypes().join(", ")}). To drive a study with a signed-in Claude Code, use type: local-agent with localAgent: claude.`);
|
|
129
34
|
}
|
|
130
|
-
|
|
131
|
-
const id = str(raw.id);
|
|
132
|
-
if (id)
|
|
133
|
-
focus.id = id;
|
|
134
|
-
const label = str(raw.label);
|
|
135
|
-
if (label)
|
|
136
|
-
focus.label = label;
|
|
137
|
-
const instruction = str(raw.instruction);
|
|
138
|
-
if (instruction)
|
|
139
|
-
focus.instruction = instruction;
|
|
140
|
-
return Object.keys(focus).length > 0 ? focus : undefined;
|
|
35
|
+
return { ok: true, value: type };
|
|
141
36
|
}
|
|
142
37
|
/**
|
|
143
|
-
*
|
|
144
|
-
*
|
|
38
|
+
* Every actor key other than `type`. The parser reads them after the participant entries, so a file
|
|
39
|
+
* with errors in both reports the entry first.
|
|
145
40
|
*/
|
|
146
|
-
function
|
|
147
|
-
|
|
148
|
-
|
|
41
|
+
export function parseActorFields(entry, path) {
|
|
42
|
+
const actor = {};
|
|
43
|
+
const persona = str(entry.persona);
|
|
44
|
+
if (persona)
|
|
45
|
+
actor.persona = persona;
|
|
46
|
+
const mission = str(entry.mission);
|
|
47
|
+
if (mission)
|
|
48
|
+
actor.mission = mission;
|
|
49
|
+
const model = str(entry.model);
|
|
50
|
+
if (model)
|
|
51
|
+
actor.model = model;
|
|
52
|
+
if (entry.maxOutputTokens !== undefined) {
|
|
53
|
+
if (!isMaxOutputTokens(entry.maxOutputTokens))
|
|
54
|
+
return invalid(`${path}.maxOutputTokens must be a positive safe integer.`);
|
|
55
|
+
actor.maxOutputTokens = entry.maxOutputTokens;
|
|
149
56
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const seenGroupIds = new Set();
|
|
155
|
-
for (const [groupIndex, entry] of raw.entries()) {
|
|
156
|
-
if (!isRecord(entry)) {
|
|
157
|
-
return invalid(`actors[${actorIndex}].roster[${groupIndex}] must be an object ({ id, count, actorType?, surface?, caseGroup?, persona?, device?, instruction?, target?, entry?, host?, reasoningEffort?, stopWhen?, dwell? }).`);
|
|
57
|
+
const localAgent = str(entry.localAgent);
|
|
58
|
+
if (entry.localAgent !== undefined) {
|
|
59
|
+
if (localAgent !== "codex" && localAgent !== "claude") {
|
|
60
|
+
return invalid(`${path}.localAgent must be "codex" or "claude" (the locally signed-in CLI that drives the study).`);
|
|
158
61
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
return invalid(`actors[${actorIndex}].roster[${groupIndex}].id must match ${PARTICIPANT_ID_PATTERN} and be at most ${PARTICIPANT_ID_MAX_CHARS - 3} characters, because each generated participant id adds a suffix like "-01"; "${groupId}" is not.`);
|
|
165
|
-
}
|
|
166
|
-
if (seenGroupIds.has(groupId)) {
|
|
167
|
-
return invalid(`actors[${actorIndex}].roster group ids must be unique (duplicate "${groupId}").`);
|
|
168
|
-
}
|
|
169
|
-
seenGroupIds.add(groupId);
|
|
170
|
-
const count = posInt(entry.count);
|
|
171
|
-
if (count === undefined) {
|
|
172
|
-
return invalid(`actors[${actorIndex}].roster[${groupIndex}].count is required and must be a positive integer.`);
|
|
173
|
-
}
|
|
174
|
-
const groupEntryInput = { ...entry };
|
|
175
|
-
delete groupEntryInput.id;
|
|
176
|
-
delete groupEntryInput.count;
|
|
177
|
-
for (let i = 1; i <= count; i += 1) {
|
|
178
|
-
expanded.push({
|
|
179
|
-
...groupEntryInput,
|
|
180
|
-
id: `${groupId}-${String(i).padStart(2, "0")}`,
|
|
181
|
-
});
|
|
62
|
+
actor.localAgent = localAgent;
|
|
63
|
+
}
|
|
64
|
+
if (entry.reasoningEffort !== undefined) {
|
|
65
|
+
if (!isReasoningEffort(entry.reasoningEffort)) {
|
|
66
|
+
return invalid(`${path}.reasoningEffort must be one of: ${reasoningEffortNames()}. Support is model-dependent, so a level this model does not accept fails on the first turn rather than being silently downgraded.`);
|
|
182
67
|
}
|
|
68
|
+
actor.reasoningEffort = entry.reasoningEffort;
|
|
183
69
|
}
|
|
184
|
-
|
|
70
|
+
const stopWhenResult = parseStopWhen(entry.stopWhen, `${path}.stopWhen`);
|
|
71
|
+
if (!stopWhenResult.ok)
|
|
72
|
+
return stopWhenResult;
|
|
73
|
+
if (stopWhenResult.value !== undefined)
|
|
74
|
+
actor.stopWhen = stopWhenResult.value;
|
|
75
|
+
const dwellResult = parseDwell(entry.dwell, `${path}.dwell`);
|
|
76
|
+
if (!dwellResult.ok)
|
|
77
|
+
return dwellResult;
|
|
78
|
+
if (dwellResult.value !== undefined)
|
|
79
|
+
actor.dwell = dwellResult.value;
|
|
80
|
+
const tasksResult = parseTasks(entry.tasks, `${path}.tasks`);
|
|
81
|
+
if (!tasksResult.ok)
|
|
82
|
+
return tasksResult;
|
|
83
|
+
if (tasksResult.value !== undefined)
|
|
84
|
+
actor.tasks = tasksResult.value;
|
|
85
|
+
return { ok: true, value: actor };
|
|
185
86
|
}
|
|
186
87
|
/**
|
|
187
|
-
* Parse
|
|
188
|
-
*
|
|
189
|
-
* entry?,
|
|
88
|
+
* Parse a participant list into a fan-out roster (computer-use E2B route): `participants` in a
|
|
89
|
+
* study, `actors[0].lanes` in the v2 file migrate reads, each named in messages by `paths`.
|
|
90
|
+
* Structural only: each entry is `{ id?, actorType?, surface?, caseGroup?, persona?, device?,
|
|
91
|
+
* instruction?, target?, entry?, host?, reasoningEffort?, stopWhen?, dwell? }`.
|
|
190
92
|
* Participant ids (when declared) must be public-safe path tokens and unique; grouping metadata
|
|
191
93
|
* must be public-safe tokens; a device must be a known preset name. The
|
|
192
|
-
* route-scoped cross-validation (
|
|
193
|
-
*
|
|
94
|
+
* route-scoped cross-validation (device XOR raw resolution, targets, cap 16) runs in parseStudy
|
|
95
|
+
* where the route is known.
|
|
194
96
|
*/
|
|
195
|
-
function parseParticipantEntries(raw,
|
|
97
|
+
export function parseParticipantEntries(raw, paths) {
|
|
196
98
|
if (raw === undefined) {
|
|
197
99
|
return { ok: true, value: undefined };
|
|
198
100
|
}
|
|
199
101
|
if (!Array.isArray(raw) || raw.length === 0) {
|
|
200
|
-
return invalid(
|
|
102
|
+
return invalid(`${paths.list} must be a non-empty array of participant objects ({ id?, actorType?, surface?, caseGroup?, persona?, device?, instruction?, target?, entry?, host?, reasoningEffort?, stopWhen?, dwell? }) when set.`);
|
|
201
103
|
}
|
|
202
104
|
const entries = [];
|
|
203
105
|
const seenIds = new Set();
|
|
204
106
|
for (const [entryIndex, entry] of raw.entries()) {
|
|
205
107
|
if (!isRecord(entry)) {
|
|
206
|
-
return invalid(
|
|
108
|
+
return invalid(`${paths.entry(entryIndex)} must be an object ({ id?, actorType?, surface?, caseGroup?, persona?, device?, instruction?, target?, entry?, host?, reasoningEffort?, stopWhen?, dwell? }).`);
|
|
207
109
|
}
|
|
208
110
|
const parsedEntry = {};
|
|
209
111
|
const id = str(entry.id);
|
|
210
112
|
if (id !== undefined) {
|
|
211
113
|
if (!PARTICIPANT_ID_PATTERN.test(id) || id.length > PARTICIPANT_ID_MAX_CHARS) {
|
|
212
|
-
return invalid(
|
|
114
|
+
return invalid(`${paths.entry(entryIndex)}.id must be a public-safe token matching ${PARTICIPANT_ID_PATTERN} and at most ${PARTICIPANT_ID_MAX_CHARS} chars (it names the participant's evidence paths); got "${id}".`);
|
|
213
115
|
}
|
|
214
116
|
if (seenIds.has(id)) {
|
|
215
|
-
return invalid(
|
|
117
|
+
return invalid(`${paths.list} ids must be unique (duplicate "${id}").`);
|
|
216
118
|
}
|
|
217
119
|
seenIds.add(id);
|
|
218
120
|
parsedEntry.id = id;
|
|
@@ -220,24 +122,24 @@ function parseParticipantEntries(raw, actorIndex) {
|
|
|
220
122
|
const device = str(entry.device);
|
|
221
123
|
if (device !== undefined) {
|
|
222
124
|
if (!isDevicePresetName(device)) {
|
|
223
|
-
return invalid(
|
|
125
|
+
return invalid(`${paths.entry(entryIndex)}.device must be one of: ${DEVICE_PRESET_NAMES.join(", ")}.`);
|
|
224
126
|
}
|
|
225
127
|
parsedEntry.device = device;
|
|
226
128
|
}
|
|
227
129
|
const persona = str(entry.persona);
|
|
228
130
|
if (persona !== undefined)
|
|
229
131
|
parsedEntry.persona = persona;
|
|
230
|
-
const actorType = parseEntryMetadata(entry.actorType,
|
|
132
|
+
const actorType = parseEntryMetadata(entry.actorType, `${paths.entry(entryIndex)}.actorType`);
|
|
231
133
|
if (!actorType.ok)
|
|
232
134
|
return actorType;
|
|
233
135
|
if (actorType.value !== undefined)
|
|
234
136
|
parsedEntry.actorType = actorType.value;
|
|
235
|
-
const surface = parseEntryMetadata(entry.surface,
|
|
137
|
+
const surface = parseEntryMetadata(entry.surface, `${paths.entry(entryIndex)}.surface`);
|
|
236
138
|
if (!surface.ok)
|
|
237
139
|
return surface;
|
|
238
140
|
if (surface.value !== undefined)
|
|
239
141
|
parsedEntry.surface = surface.value;
|
|
240
|
-
const caseGroup = parseEntryMetadata(entry.caseGroup,
|
|
142
|
+
const caseGroup = parseEntryMetadata(entry.caseGroup, `${paths.entry(entryIndex)}.caseGroup`);
|
|
241
143
|
if (!caseGroup.ok)
|
|
242
144
|
return caseGroup;
|
|
243
145
|
if (caseGroup.value !== undefined)
|
|
@@ -245,28 +147,28 @@ function parseParticipantEntries(raw, actorIndex) {
|
|
|
245
147
|
const instruction = str(entry.instruction);
|
|
246
148
|
if (instruction !== undefined)
|
|
247
149
|
parsedEntry.instruction = instruction;
|
|
248
|
-
const stopWhenResult = parseStopWhen(entry.stopWhen,
|
|
150
|
+
const stopWhenResult = parseStopWhen(entry.stopWhen, `${paths.entry(entryIndex)}.stopWhen`);
|
|
249
151
|
if (!stopWhenResult.ok)
|
|
250
152
|
return stopWhenResult;
|
|
251
153
|
if (stopWhenResult.value !== undefined)
|
|
252
154
|
parsedEntry.stopWhen = stopWhenResult.value;
|
|
253
|
-
const dwellResult = parseDwell(entry.dwell,
|
|
155
|
+
const dwellResult = parseDwell(entry.dwell, `${paths.entry(entryIndex)}.dwell`);
|
|
254
156
|
if (!dwellResult.ok)
|
|
255
157
|
return dwellResult;
|
|
256
158
|
if (dwellResult.value !== undefined)
|
|
257
159
|
parsedEntry.dwell = dwellResult.value;
|
|
258
160
|
if (entry.reasoningEffort !== undefined) {
|
|
259
161
|
if (!isReasoningEffort(entry.reasoningEffort)) {
|
|
260
|
-
return invalid(
|
|
162
|
+
return invalid(`${paths.entry(entryIndex)}.reasoningEffort must be one of: ${reasoningEffortNames()}. Support is model-dependent, so a level this model does not accept fails on the first turn rather than being silently downgraded.`);
|
|
261
163
|
}
|
|
262
164
|
parsedEntry.reasoningEffort = entry.reasoningEffort;
|
|
263
165
|
}
|
|
264
166
|
const target = str(entry.target);
|
|
265
167
|
if (target !== undefined) {
|
|
266
168
|
if (!isHttpUrl(target)) {
|
|
267
|
-
return invalid(
|
|
169
|
+
return invalid(`${paths.entry(entryIndex)}.target must be an absolute http(s) URL.`);
|
|
268
170
|
}
|
|
269
|
-
const credential = urlCredentialReason(
|
|
171
|
+
const credential = urlCredentialReason(`${paths.entry(entryIndex)}.target`, target);
|
|
270
172
|
if (credential)
|
|
271
173
|
return invalid(credential);
|
|
272
174
|
parsedEntry.target = target;
|
|
@@ -275,7 +177,7 @@ function parseParticipantEntries(raw, actorIndex) {
|
|
|
275
177
|
// it runs in sharedWorldValidationReason (where the route + serve.url are known).
|
|
276
178
|
const entryPath = str(entry.entry);
|
|
277
179
|
if (entryPath !== undefined) {
|
|
278
|
-
const credential = entryCredentialReason(
|
|
180
|
+
const credential = entryCredentialReason(`${paths.entry(entryIndex)}.entry`, entryPath);
|
|
279
181
|
if (credential)
|
|
280
182
|
return invalid(credential);
|
|
281
183
|
parsedEntry.entry = entryPath;
|
|
@@ -284,7 +186,7 @@ function parseParticipantEntries(raw, actorIndex) {
|
|
|
284
186
|
// exactly-one-host check runs in externalPublicSharedWorldValidationReason (route context).
|
|
285
187
|
if (entry.host !== undefined) {
|
|
286
188
|
if (typeof entry.host !== "boolean") {
|
|
287
|
-
return invalid(
|
|
189
|
+
return invalid(`${paths.entry(entryIndex)}.host must be a boolean (marks the designated host participant on the external-public shared-world route).`);
|
|
288
190
|
}
|
|
289
191
|
if (entry.host)
|
|
290
192
|
parsedEntry.host = true;
|
|
@@ -368,7 +270,7 @@ function parseDwell(raw, field) {
|
|
|
368
270
|
...(whenResult.value === undefined ? {} : { when: whenResult.value }),
|
|
369
271
|
ms,
|
|
370
272
|
everyMs,
|
|
371
|
-
// oxlint-disable-next-line unicorn/no-thenable -- `then` is the documented dwell field of
|
|
273
|
+
// oxlint-disable-next-line unicorn/no-thenable -- `then` is the documented dwell field of a study
|
|
372
274
|
then,
|
|
373
275
|
},
|
|
374
276
|
};
|
|
@@ -76,9 +76,13 @@ function parseCommsEmail(raw) {
|
|
|
76
76
|
}
|
|
77
77
|
return { ok: true, value: email };
|
|
78
78
|
}
|
|
79
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* A real inbox: a saved connection, with optional link and allowed origins and nothing else. The
|
|
81
|
+
* parsed config adds `kind: real`, so a config parseStudy returned parses again with it.
|
|
82
|
+
*/
|
|
80
83
|
function parseEmailConnection(raw) {
|
|
81
|
-
const unsupported = Object.keys(raw).filter((key) => !["connection", "linkOrigin", "allowedOrigins"].includes(key)
|
|
84
|
+
const unsupported = Object.keys(raw).filter((key) => !["connection", "linkOrigin", "allowedOrigins"].includes(key) &&
|
|
85
|
+
!(key === "kind" && raw.kind === "real"));
|
|
82
86
|
if (unsupported.length)
|
|
83
87
|
return invalid("An email connection cannot be mixed with capture settings, kind, recipients, or provider options. Use only connection, optional linkOrigin and allowedOrigins.");
|
|
84
88
|
if (typeof raw.connection !== "string" || !/^[a-z][a-z0-9-]{0,47}$/.test(raw.connection))
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import type { StudyParseFailure, StudyDefaults, StudyExecution, StudyPolicies, StudyReview
|
|
1
|
+
import type { StudyCaps, StudyParseFailure, StudyDefaults, StudyExecution, StudyPolicies, StudyReview } from "../types.js";
|
|
2
2
|
export declare function parseExecution(raw: unknown): {
|
|
3
3
|
ok: true;
|
|
4
4
|
value: StudyExecution | undefined;
|
|
5
5
|
} | StudyParseFailure;
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Parse a `caps` block's values. Returns a parse failure on a malformed value rather than silently
|
|
8
|
+
* dropping a budget declaration (a cap that silently does nothing would claim protection it
|
|
9
|
+
* does not give). Each cap must be a non-negative finite number. readStudyFront has checked the
|
|
10
|
+
* keys against the route; migrate checks a v2 file's two caps blocks with it.
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseCaps(raw: unknown): {
|
|
8
13
|
ok: true;
|
|
9
|
-
value:
|
|
14
|
+
value: StudyCaps | undefined;
|
|
10
15
|
} | StudyParseFailure;
|
|
11
16
|
export declare function parsePolicies(raw: unknown): {
|
|
12
17
|
ok: true;
|
|
@@ -33,14 +33,6 @@ export function parseExecution(raw) {
|
|
|
33
33
|
}
|
|
34
34
|
if (desktopResult.value)
|
|
35
35
|
execution.desktop = desktopResult.value;
|
|
36
|
-
// Reuse the terminal route's caps parser (same shape, not a fork); a malformed budget is a hard
|
|
37
|
-
// error, never silently dropped (a cap that silently does nothing would be a safety lie).
|
|
38
|
-
const capsResult = parseCaps(raw.caps);
|
|
39
|
-
if (!capsResult.ok) {
|
|
40
|
-
return capsResult;
|
|
41
|
-
}
|
|
42
|
-
if (capsResult.value)
|
|
43
|
-
execution.caps = capsResult.value;
|
|
44
36
|
const terminalResult = parseTerminal(raw.terminal);
|
|
45
37
|
if (!terminalResult.ok) {
|
|
46
38
|
return terminalResult;
|
|
@@ -217,45 +209,18 @@ function parseDesktop(raw) {
|
|
|
217
209
|
}
|
|
218
210
|
return { ok: true, value: Object.keys(desktop).length > 0 ? desktop : undefined };
|
|
219
211
|
}
|
|
220
|
-
export function parsePersonas(raw) {
|
|
221
|
-
if (!Array.isArray(raw)) {
|
|
222
|
-
return undefined;
|
|
223
|
-
}
|
|
224
|
-
const personas = raw.filter(isRecord);
|
|
225
|
-
return personas.length > 0 ? personas : undefined;
|
|
226
|
-
}
|
|
227
|
-
export function parseScenario(raw) {
|
|
228
|
-
if (!isRecord(raw)) {
|
|
229
|
-
return { ok: true, value: undefined };
|
|
230
|
-
}
|
|
231
|
-
const scenario = {};
|
|
232
|
-
const ref = str(raw.ref);
|
|
233
|
-
if (ref)
|
|
234
|
-
scenario.ref = ref;
|
|
235
|
-
if (isRecord(raw.inline))
|
|
236
|
-
scenario.inline = raw.inline;
|
|
237
|
-
const mode = str(raw.mode);
|
|
238
|
-
if (mode === "dry-run" || mode === "live")
|
|
239
|
-
scenario.mode = mode;
|
|
240
|
-
const capsResult = parseCaps(raw.caps);
|
|
241
|
-
if (!capsResult.ok) {
|
|
242
|
-
return capsResult;
|
|
243
|
-
}
|
|
244
|
-
if (capsResult.value)
|
|
245
|
-
scenario.caps = capsResult.value;
|
|
246
|
-
return { ok: true, value: Object.keys(scenario).length > 0 ? scenario : undefined };
|
|
247
|
-
}
|
|
248
212
|
/**
|
|
249
|
-
* Parse `
|
|
213
|
+
* Parse a `caps` block's values. Returns a parse failure on a malformed value rather than silently
|
|
250
214
|
* dropping a budget declaration (a cap that silently does nothing would claim protection it
|
|
251
|
-
* does not give). Each cap must be a non-negative finite number.
|
|
215
|
+
* does not give). Each cap must be a non-negative finite number. readStudyFront has checked the
|
|
216
|
+
* keys against the route; migrate checks a v2 file's two caps blocks with it.
|
|
252
217
|
*/
|
|
253
|
-
function parseCaps(raw) {
|
|
218
|
+
export function parseCaps(raw) {
|
|
254
219
|
if (raw === undefined) {
|
|
255
220
|
return { ok: true, value: undefined };
|
|
256
221
|
}
|
|
257
222
|
if (!isRecord(raw)) {
|
|
258
|
-
return invalid("`
|
|
223
|
+
return invalid("`caps` must be an object ({ maxUsd?, maxTotalUsd?, maxJobs?, maxMinutes? }).");
|
|
259
224
|
}
|
|
260
225
|
const caps = {};
|
|
261
226
|
for (const key of ["maxUsd", "maxTotalUsd", "maxJobs", "maxMinutes"]) {
|
|
@@ -263,7 +228,7 @@ function parseCaps(raw) {
|
|
|
263
228
|
continue;
|
|
264
229
|
const value = nonNegNumber(raw[key]);
|
|
265
230
|
if (value === undefined) {
|
|
266
|
-
return invalid(`\`
|
|
231
|
+
return invalid(`\`caps.${key}\` must be a non-negative number.`);
|
|
267
232
|
}
|
|
268
233
|
caps[key] = value;
|
|
269
234
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { StudyRoute } from "../routing.js";
|
|
2
|
+
import type { StudyParseFailure, StudySurfaces } from "../types.js";
|
|
3
|
+
/** A document's `participants`, read before the participant entries parse. */
|
|
4
|
+
export interface Participants {
|
|
5
|
+
count?: number;
|
|
6
|
+
instruction?: unknown;
|
|
7
|
+
/** The list entries, with each entry that has a count expanded into its participants. */
|
|
8
|
+
entries?: unknown[];
|
|
9
|
+
/** For each expanded entry, the index of the `participants` entry it came from. */
|
|
10
|
+
source: number[];
|
|
11
|
+
}
|
|
12
|
+
/** What a v3 document declares before its sections parse. */
|
|
13
|
+
export interface StudyFront {
|
|
14
|
+
readonly route: StudyRoute;
|
|
15
|
+
readonly actor: Record<string, unknown>;
|
|
16
|
+
readonly participants: Participants;
|
|
17
|
+
readonly surfaces: StudySurfaces | undefined;
|
|
18
|
+
}
|
|
19
|
+
type Parsed<T> = {
|
|
20
|
+
ok: true;
|
|
21
|
+
value: T;
|
|
22
|
+
} | StudyParseFailure;
|
|
23
|
+
/** The refusal for a study with no `actor` object. */
|
|
24
|
+
export declare const ACTOR_REQUIRED = "A study needs `actor:`, an object with at least `type`.";
|
|
25
|
+
/** Why `route` is not a route a study can declare, or undefined when it is one. */
|
|
26
|
+
export declare function routeNameReason(route: unknown): string | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* The checks a v3 document passes before its sections parse: no v2 key and no unknown key, a
|
|
29
|
+
* route, a mode, an `actor` object, a scenario string, and the `participants`, `surfaces` and
|
|
30
|
+
* `caps` keys its route takes. `anySharedWorldParticipants`, for migrate's v3 form of a v2 file:
|
|
31
|
+
* a shared world takes `participants` in every form computer use takes, or none, because a v2
|
|
32
|
+
* shared world could declare a count or no list. The shared-world checks then refuse it, as the v2
|
|
33
|
+
* parser does.
|
|
34
|
+
*/
|
|
35
|
+
export declare function readStudyFront(raw: Record<string, unknown>, anySharedWorldParticipants?: boolean): Parsed<StudyFront>;
|
|
36
|
+
export {};
|