@aloud/runner 0.2.3 → 0.2.5
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/dist/cli.js +548 -224
- package/package.json +1 -1
- package/src/cli.ts +194 -16
- package/src/run/execute.ts +17 -1
- package/src/version.ts +1 -1
package/dist/cli.js
CHANGED
|
@@ -59,6 +59,16 @@ var init_ids = __esm({
|
|
|
59
59
|
ledgerEntry: "led",
|
|
60
60
|
// BILLING_PLAN 2.1: a grant of credit with its own expiry, which is what a balance is made of.
|
|
61
61
|
creditLot: "clt",
|
|
62
|
+
// BILLING_PLAN 3: the estimate range and approved ceiling a run was authorised under. Stored
|
|
63
|
+
// rather than displayed, because a variable meter with no recorded agreement is a dispute.
|
|
64
|
+
quote: "qte",
|
|
65
|
+
// BILLING_PLAN 4: the billing period a workspace was on. The tier belongs to the period rather
|
|
66
|
+
// than to the workspace row, because a workspace outlives its plan.
|
|
67
|
+
plan: "pln",
|
|
68
|
+
// BILLING_PLAN 4: one meter event owed to Stripe. This is the row identity the drain worker
|
|
69
|
+
// uses, NOT what is sent as the dedup identifier: that is the ledger entry id the row carries,
|
|
70
|
+
// so that Stripe deduplication and the local unique index agree on what a duplicate is.
|
|
71
|
+
outboxEvent: "obx",
|
|
62
72
|
rateCard: "rtc",
|
|
63
73
|
auditEvent: "aud",
|
|
64
74
|
job: "job",
|
|
@@ -1107,41 +1117,110 @@ var init_moment = __esm({
|
|
|
1107
1117
|
}
|
|
1108
1118
|
});
|
|
1109
1119
|
|
|
1110
|
-
// ../core/src/schemas/
|
|
1120
|
+
// ../core/src/schemas/session.ts
|
|
1111
1121
|
import { z as z9 } from "zod";
|
|
1122
|
+
var SessionLimits, SessionError, SessionEndReason, Session;
|
|
1123
|
+
var init_session = __esm({
|
|
1124
|
+
"../core/src/schemas/session.ts"() {
|
|
1125
|
+
"use strict";
|
|
1126
|
+
init_persona();
|
|
1127
|
+
init_patience();
|
|
1128
|
+
init_machines();
|
|
1129
|
+
init_action();
|
|
1130
|
+
SessionLimits = z9.object({
|
|
1131
|
+
maxActions: z9.number().int().positive(),
|
|
1132
|
+
maxElapsedMs: z9.number().int().positive(),
|
|
1133
|
+
maxTokens: z9.number().int().positive()
|
|
1134
|
+
});
|
|
1135
|
+
SessionError = z9.object({
|
|
1136
|
+
kind: z9.enum(["browser", "model", "policy", "timeout", "storage", "internal"]),
|
|
1137
|
+
message: z9.string(),
|
|
1138
|
+
retryable: z9.boolean(),
|
|
1139
|
+
occurredAt: z9.string()
|
|
1140
|
+
});
|
|
1141
|
+
SessionEndReason = z9.enum([
|
|
1142
|
+
"finished",
|
|
1143
|
+
"abandoned",
|
|
1144
|
+
"cap_actions",
|
|
1145
|
+
"cap_elapsed",
|
|
1146
|
+
"cap_tokens",
|
|
1147
|
+
"error",
|
|
1148
|
+
"canceled"
|
|
1149
|
+
]);
|
|
1150
|
+
Session = z9.object({
|
|
1151
|
+
id: z9.string(),
|
|
1152
|
+
runId: z9.string(),
|
|
1153
|
+
workspaceId: z9.string(),
|
|
1154
|
+
persona: PersonaBrief,
|
|
1155
|
+
personaId: z9.string().nullable(),
|
|
1156
|
+
status: SessionStatus,
|
|
1157
|
+
/** SPEC FR-054: recorded as evidence, never the reported result. */
|
|
1158
|
+
selfReportedOutcome: SessionOutcome.nullable(),
|
|
1159
|
+
selfReportedReason: z9.string().nullable(),
|
|
1160
|
+
patienceBudget: PatienceBudget,
|
|
1161
|
+
patienceState: PatienceState,
|
|
1162
|
+
limits: SessionLimits,
|
|
1163
|
+
startedAt: z9.string().nullable(),
|
|
1164
|
+
endedAt: z9.string().nullable(),
|
|
1165
|
+
error: SessionError.nullable(),
|
|
1166
|
+
/** SPEC 14.4: set when integrity checks exclude or flag this session. */
|
|
1167
|
+
contamination: z9.object({
|
|
1168
|
+
contaminated: z9.boolean(),
|
|
1169
|
+
flaggedOnly: z9.boolean(),
|
|
1170
|
+
signals: z9.array(z9.string())
|
|
1171
|
+
}).nullable(),
|
|
1172
|
+
momentCount: z9.number().int().nonnegative().default(0),
|
|
1173
|
+
/**
|
|
1174
|
+
* Why the loop stopped. Null only for a session recorded before this field existed.
|
|
1175
|
+
*
|
|
1176
|
+
* Nullable rather than defaulted, per BILLING_PLAN Part 4 ("new nullable columns", additive only):
|
|
1177
|
+
* a historical session has no honest value, and inventing one from its status would manufacture
|
|
1178
|
+
* exactly the signal this field exists to stop anybody guessing at. Null therefore means "not
|
|
1179
|
+
* recorded", never "ended cleanly".
|
|
1180
|
+
*/
|
|
1181
|
+
endReason: SessionEndReason.nullable(),
|
|
1182
|
+
/** Which browser worker ran it, for the isolation audit trail (SPEC FR-040). */
|
|
1183
|
+
workerId: z9.string().nullable()
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
// ../core/src/schemas/study.ts
|
|
1189
|
+
import { z as z10 } from "zod";
|
|
1112
1190
|
var EnvironmentType, ActionPolicy, Viewport, VIEWPORT_PRESETS, Environment, Product, SuccessCriterion, StudyStatus, Study, StudySnapshot;
|
|
1113
1191
|
var init_study = __esm({
|
|
1114
1192
|
"../core/src/schemas/study.ts"() {
|
|
1115
1193
|
"use strict";
|
|
1116
1194
|
init_persona();
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1195
|
+
init_session();
|
|
1196
|
+
EnvironmentType = z10.enum(["production", "staging", "preview", "local", "benchmark"]);
|
|
1197
|
+
ActionPolicy = z10.object({
|
|
1198
|
+
blockConsequentialActions: z10.boolean().default(true),
|
|
1199
|
+
allowFormSubmission: z10.boolean().default(false),
|
|
1200
|
+
allowedDomains: z10.array(z10.string()).default([]),
|
|
1122
1201
|
/** SPEC FR-014: redirects outside allowed domains are blocked or need approval. */
|
|
1123
|
-
blockOffDomainNavigation:
|
|
1202
|
+
blockOffDomainNavigation: z10.boolean().default(true),
|
|
1124
1203
|
/** SPEC FR-050/FR-055: raw secret entry by the participant is always blocked. */
|
|
1125
|
-
allowSecretEntry:
|
|
1204
|
+
allowSecretEntry: z10.literal(false).default(false)
|
|
1126
1205
|
});
|
|
1127
|
-
Viewport =
|
|
1128
|
-
width:
|
|
1129
|
-
height:
|
|
1130
|
-
deviceScaleFactor:
|
|
1131
|
-
isMobile:
|
|
1206
|
+
Viewport = z10.object({
|
|
1207
|
+
width: z10.number().int().min(320).max(3840),
|
|
1208
|
+
height: z10.number().int().min(480).max(2160),
|
|
1209
|
+
deviceScaleFactor: z10.number().min(1).max(3).default(1),
|
|
1210
|
+
isMobile: z10.boolean().default(false)
|
|
1132
1211
|
});
|
|
1133
1212
|
VIEWPORT_PRESETS = {
|
|
1134
1213
|
desktop: { width: 1440, height: 900, deviceScaleFactor: 1, isMobile: false },
|
|
1135
1214
|
mobile_web: { width: 390, height: 844, deviceScaleFactor: 2, isMobile: true }
|
|
1136
1215
|
};
|
|
1137
|
-
Environment =
|
|
1138
|
-
id:
|
|
1139
|
-
workspaceId:
|
|
1140
|
-
productId:
|
|
1141
|
-
name:
|
|
1142
|
-
baseUrl:
|
|
1216
|
+
Environment = z10.object({
|
|
1217
|
+
id: z10.string(),
|
|
1218
|
+
workspaceId: z10.string(),
|
|
1219
|
+
productId: z10.string(),
|
|
1220
|
+
name: z10.string().min(1).max(120),
|
|
1221
|
+
baseUrl: z10.string().url(),
|
|
1143
1222
|
type: EnvironmentType,
|
|
1144
|
-
allowedDomains:
|
|
1223
|
+
allowedDomains: z10.array(z10.string()).default([]),
|
|
1145
1224
|
defaultViewport: DeviceContext.default("desktop"),
|
|
1146
1225
|
actionPolicy: ActionPolicy,
|
|
1147
1226
|
/**
|
|
@@ -1149,147 +1228,163 @@ var init_study = __esm({
|
|
|
1149
1228
|
* A promise that secrets never appear in a screenshot is not enforceable against a live app, so
|
|
1150
1229
|
* the policy is environment-scoped rather than absolute.
|
|
1151
1230
|
*/
|
|
1152
|
-
redactionRequired:
|
|
1153
|
-
createdAt:
|
|
1231
|
+
redactionRequired: z10.boolean(),
|
|
1232
|
+
createdAt: z10.string(),
|
|
1154
1233
|
/** Marked when the system inferred this rather than the user configuring it (SPEC 9.0). */
|
|
1155
|
-
inferred:
|
|
1234
|
+
inferred: z10.boolean().default(false)
|
|
1156
1235
|
});
|
|
1157
|
-
Product =
|
|
1158
|
-
id:
|
|
1159
|
-
workspaceId:
|
|
1160
|
-
name:
|
|
1161
|
-
description:
|
|
1162
|
-
createdAt:
|
|
1163
|
-
inferred:
|
|
1236
|
+
Product = z10.object({
|
|
1237
|
+
id: z10.string(),
|
|
1238
|
+
workspaceId: z10.string(),
|
|
1239
|
+
name: z10.string().min(1).max(120),
|
|
1240
|
+
description: z10.string().max(2e3).default(""),
|
|
1241
|
+
createdAt: z10.string(),
|
|
1242
|
+
inferred: z10.boolean().default(false)
|
|
1164
1243
|
});
|
|
1165
|
-
SuccessCriterion =
|
|
1166
|
-
id:
|
|
1244
|
+
SuccessCriterion = z10.object({
|
|
1245
|
+
id: z10.string(),
|
|
1167
1246
|
/** What a judge could confirm from a screenshot, in plain language. */
|
|
1168
|
-
statement:
|
|
1247
|
+
statement: z10.string().min(1).max(400),
|
|
1169
1248
|
/** Which visible signal confirms it. */
|
|
1170
|
-
observableSignal:
|
|
1171
|
-
required:
|
|
1249
|
+
observableSignal: z10.string().min(1).max(400),
|
|
1250
|
+
required: z10.boolean().default(true),
|
|
1172
1251
|
/** Marked when the system proposed it (SPEC FR-026). */
|
|
1173
|
-
proposed:
|
|
1252
|
+
proposed: z10.boolean().default(false)
|
|
1174
1253
|
});
|
|
1175
|
-
StudyStatus =
|
|
1176
|
-
Study =
|
|
1177
|
-
id:
|
|
1178
|
-
workspaceId:
|
|
1179
|
-
productId:
|
|
1180
|
-
environmentId:
|
|
1181
|
-
name:
|
|
1254
|
+
StudyStatus = z10.enum(["draft", "ready", "archived"]);
|
|
1255
|
+
Study = z10.object({
|
|
1256
|
+
id: z10.string(),
|
|
1257
|
+
workspaceId: z10.string(),
|
|
1258
|
+
productId: z10.string(),
|
|
1259
|
+
environmentId: z10.string(),
|
|
1260
|
+
name: z10.string().min(1).max(160),
|
|
1182
1261
|
/** SPEC FR-020: the flow under test. */
|
|
1183
|
-
targetFlow:
|
|
1184
|
-
scenario:
|
|
1262
|
+
targetFlow: z10.string().min(1).max(400),
|
|
1263
|
+
scenario: z10.string().min(1).max(2e3),
|
|
1185
1264
|
/** In user language. Never a step-by-step script (SPEC 26 research integrity). */
|
|
1186
|
-
goal:
|
|
1187
|
-
successCriteria:
|
|
1265
|
+
goal: z10.string().min(1).max(600),
|
|
1266
|
+
successCriteria: z10.array(SuccessCriterion).min(1),
|
|
1188
1267
|
device: DeviceContext,
|
|
1189
|
-
participantCount:
|
|
1268
|
+
participantCount: z10.number().int().min(1).max(12),
|
|
1190
1269
|
/** Drives patience budget compilation (SPEC 14.5). */
|
|
1191
|
-
expectedSteps:
|
|
1192
|
-
startUrl:
|
|
1270
|
+
expectedSteps: z10.number().int().min(1).max(60).default(6),
|
|
1271
|
+
startUrl: z10.string().url(),
|
|
1193
1272
|
status: StudyStatus,
|
|
1194
|
-
createdAt:
|
|
1195
|
-
updatedAt:
|
|
1196
|
-
inferred:
|
|
1273
|
+
createdAt: z10.string(),
|
|
1274
|
+
updatedAt: z10.string(),
|
|
1275
|
+
inferred: z10.boolean().default(false)
|
|
1197
1276
|
});
|
|
1198
|
-
StudySnapshot =
|
|
1277
|
+
StudySnapshot = z10.object({
|
|
1199
1278
|
study: Study,
|
|
1200
1279
|
environment: Environment,
|
|
1201
|
-
cast:
|
|
1280
|
+
cast: z10.array(PersonaBrief).min(1),
|
|
1202
1281
|
viewport: Viewport,
|
|
1203
|
-
promptVersion:
|
|
1204
|
-
actionSchemaVersion:
|
|
1205
|
-
|
|
1282
|
+
promptVersion: z10.string(),
|
|
1283
|
+
actionSchemaVersion: z10.string(),
|
|
1284
|
+
/**
|
|
1285
|
+
* BILLING_PLAN 3: the per-session ceiling this run was authorised under, derived from the
|
|
1286
|
+
* approved quote's cap by `maxActionsForCap`.
|
|
1287
|
+
*
|
|
1288
|
+
* It belongs in the snapshot for the same reason the cast and the environment do. FR-025 freezes
|
|
1289
|
+
* a run's configuration so that editing the study afterwards cannot change what an existing
|
|
1290
|
+
* report claims was tested, and the ceiling an operator approved is part of that configuration:
|
|
1291
|
+
* re-quoting a study next month must not retroactively widen or narrow a run that already
|
|
1292
|
+
* happened. It is also what carries the cap to a local runner, which works from the lease's
|
|
1293
|
+
* snapshot and never sees `study_quotes`.
|
|
1294
|
+
*
|
|
1295
|
+
* Optional, and null-safe on read. Runs that predate BILLING_PLAN 3 carry no quote, and a stored
|
|
1296
|
+
* snapshot is not rewritten to add one; those fall back to the platform default in the
|
|
1297
|
+
* coordinator rather than being refused years later.
|
|
1298
|
+
*/
|
|
1299
|
+
sessionLimits: SessionLimits.optional(),
|
|
1300
|
+
capturedAt: z10.string()
|
|
1206
1301
|
});
|
|
1207
1302
|
}
|
|
1208
1303
|
});
|
|
1209
1304
|
|
|
1210
1305
|
// ../core/src/schemas/product-context.ts
|
|
1211
|
-
import { z as
|
|
1306
|
+
import { z as z11 } from "zod";
|
|
1212
1307
|
var ProductEvidenceKind, ProductEvidenceRef, AudienceSegment, ProductFlow, ProductProfile, PersonaProposal, DiscoveryLink, DiscoveryPage, ProductDiscoveryEvidence, StudySetupStatus, StudySetupJob;
|
|
1213
1308
|
var init_product_context = __esm({
|
|
1214
1309
|
"../core/src/schemas/product-context.ts"() {
|
|
1215
1310
|
"use strict";
|
|
1216
1311
|
init_persona();
|
|
1217
|
-
ProductEvidenceKind =
|
|
1312
|
+
ProductEvidenceKind = z11.enum([
|
|
1218
1313
|
"rendered_page",
|
|
1219
1314
|
"user_statement",
|
|
1220
1315
|
"agent_summary",
|
|
1221
1316
|
"local_manifest"
|
|
1222
1317
|
]);
|
|
1223
|
-
ProductEvidenceRef =
|
|
1224
|
-
id:
|
|
1318
|
+
ProductEvidenceRef = z11.object({
|
|
1319
|
+
id: z11.string(),
|
|
1225
1320
|
kind: ProductEvidenceKind,
|
|
1226
1321
|
/** A human-readable source name, such as "Pricing" or "Project summary". */
|
|
1227
|
-
label:
|
|
1322
|
+
label: z11.string().min(1).max(160),
|
|
1228
1323
|
/** URL for rendered evidence; a non-sensitive logical locator for local context. */
|
|
1229
|
-
locator:
|
|
1324
|
+
locator: z11.string().min(1).max(1e3),
|
|
1230
1325
|
/** Only customer-facing copy may be retained here. Raw repository source never belongs here. */
|
|
1231
|
-
excerpt:
|
|
1232
|
-
contentHash:
|
|
1233
|
-
capturedAt:
|
|
1326
|
+
excerpt: z11.string().max(800).nullable().default(null),
|
|
1327
|
+
contentHash: z11.string().max(128).nullable().default(null),
|
|
1328
|
+
capturedAt: z11.string()
|
|
1234
1329
|
});
|
|
1235
|
-
AudienceSegment =
|
|
1236
|
-
id:
|
|
1237
|
-
label:
|
|
1238
|
-
description:
|
|
1239
|
-
roles:
|
|
1240
|
-
jobsToBeDone:
|
|
1241
|
-
evidenceIds:
|
|
1242
|
-
confidence:
|
|
1330
|
+
AudienceSegment = z11.object({
|
|
1331
|
+
id: z11.string(),
|
|
1332
|
+
label: z11.string().min(1).max(120),
|
|
1333
|
+
description: z11.string().min(1).max(600),
|
|
1334
|
+
roles: z11.array(z11.string().min(1).max(120)).max(12).default([]),
|
|
1335
|
+
jobsToBeDone: z11.array(z11.string().min(1).max(400)).max(12).default([]),
|
|
1336
|
+
evidenceIds: z11.array(z11.string()).min(1),
|
|
1337
|
+
confidence: z11.number().min(0).max(1)
|
|
1243
1338
|
});
|
|
1244
|
-
ProductFlow =
|
|
1245
|
-
label:
|
|
1246
|
-
outcome:
|
|
1247
|
-
evidenceIds:
|
|
1339
|
+
ProductFlow = z11.object({
|
|
1340
|
+
label: z11.string().min(1).max(160),
|
|
1341
|
+
outcome: z11.string().min(1).max(400),
|
|
1342
|
+
evidenceIds: z11.array(z11.string()).min(1)
|
|
1248
1343
|
});
|
|
1249
|
-
ProductProfile =
|
|
1250
|
-
id:
|
|
1251
|
-
workspaceId:
|
|
1252
|
-
productId:
|
|
1253
|
-
version:
|
|
1254
|
-
name:
|
|
1255
|
-
category:
|
|
1256
|
-
summary:
|
|
1257
|
-
valueProposition:
|
|
1258
|
-
customerTypes:
|
|
1259
|
-
audiences:
|
|
1260
|
-
keyFlows:
|
|
1261
|
-
domainVocabulary:
|
|
1262
|
-
evidence:
|
|
1263
|
-
confidence:
|
|
1264
|
-
sourceFingerprint:
|
|
1265
|
-
supersedesId:
|
|
1266
|
-
createdBy:
|
|
1267
|
-
createdAt:
|
|
1344
|
+
ProductProfile = z11.object({
|
|
1345
|
+
id: z11.string(),
|
|
1346
|
+
workspaceId: z11.string(),
|
|
1347
|
+
productId: z11.string(),
|
|
1348
|
+
version: z11.number().int().positive(),
|
|
1349
|
+
name: z11.string().min(1).max(120),
|
|
1350
|
+
category: z11.string().min(1).max(160),
|
|
1351
|
+
summary: z11.string().min(1).max(1200),
|
|
1352
|
+
valueProposition: z11.string().max(800).default(""),
|
|
1353
|
+
customerTypes: z11.array(z11.string().min(1).max(160)).max(12).default([]),
|
|
1354
|
+
audiences: z11.array(AudienceSegment).max(16).default([]),
|
|
1355
|
+
keyFlows: z11.array(ProductFlow).max(20).default([]),
|
|
1356
|
+
domainVocabulary: z11.array(z11.string().min(1).max(100)).max(80).default([]),
|
|
1357
|
+
evidence: z11.array(ProductEvidenceRef).max(80).default([]),
|
|
1358
|
+
confidence: z11.number().min(0).max(1),
|
|
1359
|
+
sourceFingerprint: z11.string().min(1).max(128),
|
|
1360
|
+
supersedesId: z11.string().nullable(),
|
|
1361
|
+
createdBy: z11.enum(["system", "user", "agent"]),
|
|
1362
|
+
createdAt: z11.string()
|
|
1268
1363
|
});
|
|
1269
|
-
PersonaProposal =
|
|
1364
|
+
PersonaProposal = z11.object({
|
|
1270
1365
|
brief: PersonaBrief,
|
|
1271
|
-
audienceSegmentId:
|
|
1272
|
-
rationale:
|
|
1273
|
-
evidenceIds:
|
|
1274
|
-
confidence:
|
|
1366
|
+
audienceSegmentId: z11.string().nullable(),
|
|
1367
|
+
rationale: z11.string().min(1).max(600),
|
|
1368
|
+
evidenceIds: z11.array(z11.string()).default([]),
|
|
1369
|
+
confidence: z11.number().min(0).max(1)
|
|
1275
1370
|
});
|
|
1276
|
-
DiscoveryLink =
|
|
1277
|
-
href:
|
|
1278
|
-
text:
|
|
1371
|
+
DiscoveryLink = z11.object({
|
|
1372
|
+
href: z11.string().url(),
|
|
1373
|
+
text: z11.string().max(240)
|
|
1279
1374
|
});
|
|
1280
|
-
DiscoveryPage =
|
|
1281
|
-
url:
|
|
1282
|
-
title:
|
|
1283
|
-
headings:
|
|
1284
|
-
visibleText:
|
|
1285
|
-
links:
|
|
1375
|
+
DiscoveryPage = z11.object({
|
|
1376
|
+
url: z11.string().url(),
|
|
1377
|
+
title: z11.string().max(300),
|
|
1378
|
+
headings: z11.array(z11.string().max(300)).max(80),
|
|
1379
|
+
visibleText: z11.string().max(3e4),
|
|
1380
|
+
links: z11.array(DiscoveryLink).max(200)
|
|
1286
1381
|
});
|
|
1287
|
-
ProductDiscoveryEvidence =
|
|
1288
|
-
pages:
|
|
1289
|
-
sourceFingerprint:
|
|
1290
|
-
capturedAt:
|
|
1382
|
+
ProductDiscoveryEvidence = z11.object({
|
|
1383
|
+
pages: z11.array(DiscoveryPage).min(1).max(8),
|
|
1384
|
+
sourceFingerprint: z11.string().min(1).max(128),
|
|
1385
|
+
capturedAt: z11.string()
|
|
1291
1386
|
});
|
|
1292
|
-
StudySetupStatus =
|
|
1387
|
+
StudySetupStatus = z11.enum([
|
|
1293
1388
|
"awaiting_runner",
|
|
1294
1389
|
"discovering",
|
|
1295
1390
|
"analysing",
|
|
@@ -1299,77 +1394,27 @@ var init_product_context = __esm({
|
|
|
1299
1394
|
"failed",
|
|
1300
1395
|
"expired"
|
|
1301
1396
|
]);
|
|
1302
|
-
StudySetupJob =
|
|
1303
|
-
id:
|
|
1304
|
-
workspaceId:
|
|
1305
|
-
url:
|
|
1306
|
-
goal:
|
|
1397
|
+
StudySetupJob = z11.object({
|
|
1398
|
+
id: z11.string(),
|
|
1399
|
+
workspaceId: z11.string(),
|
|
1400
|
+
url: z11.string().url(),
|
|
1401
|
+
goal: z11.string().min(1).max(600),
|
|
1307
1402
|
device: DeviceContext,
|
|
1308
|
-
participantCount:
|
|
1309
|
-
allowedHosts:
|
|
1403
|
+
participantCount: z11.number().int().min(1).max(8),
|
|
1404
|
+
allowedHosts: z11.array(z11.string()).min(1),
|
|
1310
1405
|
status: StudySetupStatus,
|
|
1311
|
-
runnerId:
|
|
1406
|
+
runnerId: z11.string().nullable(),
|
|
1312
1407
|
discoveryEvidence: ProductDiscoveryEvidence.nullable(),
|
|
1313
|
-
productId:
|
|
1314
|
-
profileId:
|
|
1315
|
-
studyId:
|
|
1316
|
-
personaProposals:
|
|
1317
|
-
degradedReason:
|
|
1318
|
-
failureReason:
|
|
1319
|
-
createdAt:
|
|
1320
|
-
claimedAt:
|
|
1321
|
-
expiresAt:
|
|
1322
|
-
completedAt:
|
|
1323
|
-
});
|
|
1324
|
-
}
|
|
1325
|
-
});
|
|
1326
|
-
|
|
1327
|
-
// ../core/src/schemas/session.ts
|
|
1328
|
-
import { z as z11 } from "zod";
|
|
1329
|
-
var SessionLimits, SessionError, Session;
|
|
1330
|
-
var init_session = __esm({
|
|
1331
|
-
"../core/src/schemas/session.ts"() {
|
|
1332
|
-
"use strict";
|
|
1333
|
-
init_persona();
|
|
1334
|
-
init_patience();
|
|
1335
|
-
init_machines();
|
|
1336
|
-
init_action();
|
|
1337
|
-
SessionLimits = z11.object({
|
|
1338
|
-
maxActions: z11.number().int().positive(),
|
|
1339
|
-
maxElapsedMs: z11.number().int().positive(),
|
|
1340
|
-
maxTokens: z11.number().int().positive()
|
|
1341
|
-
});
|
|
1342
|
-
SessionError = z11.object({
|
|
1343
|
-
kind: z11.enum(["browser", "model", "policy", "timeout", "storage", "internal"]),
|
|
1344
|
-
message: z11.string(),
|
|
1345
|
-
retryable: z11.boolean(),
|
|
1346
|
-
occurredAt: z11.string()
|
|
1347
|
-
});
|
|
1348
|
-
Session = z11.object({
|
|
1349
|
-
id: z11.string(),
|
|
1350
|
-
runId: z11.string(),
|
|
1351
|
-
workspaceId: z11.string(),
|
|
1352
|
-
persona: PersonaBrief,
|
|
1353
|
-
personaId: z11.string().nullable(),
|
|
1354
|
-
status: SessionStatus,
|
|
1355
|
-
/** SPEC FR-054: recorded as evidence, never the reported result. */
|
|
1356
|
-
selfReportedOutcome: SessionOutcome.nullable(),
|
|
1357
|
-
selfReportedReason: z11.string().nullable(),
|
|
1358
|
-
patienceBudget: PatienceBudget,
|
|
1359
|
-
patienceState: PatienceState,
|
|
1360
|
-
limits: SessionLimits,
|
|
1361
|
-
startedAt: z11.string().nullable(),
|
|
1362
|
-
endedAt: z11.string().nullable(),
|
|
1363
|
-
error: SessionError.nullable(),
|
|
1364
|
-
/** SPEC 14.4: set when integrity checks exclude or flag this session. */
|
|
1365
|
-
contamination: z11.object({
|
|
1366
|
-
contaminated: z11.boolean(),
|
|
1367
|
-
flaggedOnly: z11.boolean(),
|
|
1368
|
-
signals: z11.array(z11.string())
|
|
1369
|
-
}).nullable(),
|
|
1370
|
-
momentCount: z11.number().int().nonnegative().default(0),
|
|
1371
|
-
/** Which browser worker ran it, for the isolation audit trail (SPEC FR-040). */
|
|
1372
|
-
workerId: z11.string().nullable()
|
|
1408
|
+
productId: z11.string().nullable(),
|
|
1409
|
+
profileId: z11.string().nullable(),
|
|
1410
|
+
studyId: z11.string().nullable(),
|
|
1411
|
+
personaProposals: z11.array(PersonaProposal).default([]),
|
|
1412
|
+
degradedReason: z11.string().max(1e3).nullable(),
|
|
1413
|
+
failureReason: z11.string().max(1e3).nullable(),
|
|
1414
|
+
createdAt: z11.string(),
|
|
1415
|
+
claimedAt: z11.string().nullable(),
|
|
1416
|
+
expiresAt: z11.string(),
|
|
1417
|
+
completedAt: z11.string().nullable()
|
|
1373
1418
|
});
|
|
1374
1419
|
}
|
|
1375
1420
|
});
|
|
@@ -1850,7 +1895,7 @@ var init_recommendation = __esm({
|
|
|
1850
1895
|
|
|
1851
1896
|
// ../core/src/schemas/billing.ts
|
|
1852
1897
|
import { z as z18 } from "zod";
|
|
1853
|
-
var RateCard, ChargeLineKind, ChargeLine;
|
|
1898
|
+
var RateCard, ChargeLineKind, ChargeLine, StudyEstimate, EstimateBasis, StudyQuote;
|
|
1854
1899
|
var init_billing = __esm({
|
|
1855
1900
|
"../core/src/schemas/billing.ts"() {
|
|
1856
1901
|
"use strict";
|
|
@@ -1892,6 +1937,44 @@ var init_billing = __esm({
|
|
|
1892
1937
|
*/
|
|
1893
1938
|
idempotencyKey: z18.string()
|
|
1894
1939
|
});
|
|
1940
|
+
StudyEstimate = z18.object({
|
|
1941
|
+
/** The optimistic end: participants who move through the flow faster than predicted. */
|
|
1942
|
+
low: z18.number().int().nonnegative(),
|
|
1943
|
+
/** The midpoint, priced at the predicted moment count exactly. */
|
|
1944
|
+
expected: z18.number().int().nonnegative(),
|
|
1945
|
+
/** The pessimistic end: participants who flounder, which is also where the findings are. */
|
|
1946
|
+
high: z18.number().int().nonnegative(),
|
|
1947
|
+
/** At or above `high` with headroom. See CAP_HEADROOM in ../billing.ts for the policy. */
|
|
1948
|
+
cap: z18.number().int().nonnegative()
|
|
1949
|
+
});
|
|
1950
|
+
EstimateBasis = z18.enum(["archetype", "history"]);
|
|
1951
|
+
StudyQuote = z18.object({
|
|
1952
|
+
id: z18.string(),
|
|
1953
|
+
workspaceId: z18.string(),
|
|
1954
|
+
/** The run this quote priced. Not a foreign key in the schema: an agreement outlives its run. */
|
|
1955
|
+
runId: z18.string(),
|
|
1956
|
+
/** I6: the immutable card the range was computed under, so the range stays reproducible. */
|
|
1957
|
+
rateCardVersion: z18.string(),
|
|
1958
|
+
/** Non-negative rather than positive: a zero-participant study still reaches synthesis, and a
|
|
1959
|
+
* rate card is free to price any fixed line at zero. Negative is the only impossible value. */
|
|
1960
|
+
estimateLowCredits: z18.number().int().nonnegative(),
|
|
1961
|
+
estimateHighCredits: z18.number().int().nonnegative(),
|
|
1962
|
+
/** The ceiling the operator approves, and the number SessionLimits.maxActions is derived from. */
|
|
1963
|
+
approvedCapCredits: z18.number().int().nonnegative(),
|
|
1964
|
+
/** Null until an operator agrees. Both approval fields are set together or not at all. */
|
|
1965
|
+
approvedBy: z18.string().nullable(),
|
|
1966
|
+
approvedAt: z18.string().nullable(),
|
|
1967
|
+
createdAt: z18.string()
|
|
1968
|
+
}).refine((q) => q.estimateLowCredits <= q.estimateHighCredits, {
|
|
1969
|
+
message: "estimateLowCredits must not exceed estimateHighCredits",
|
|
1970
|
+
path: ["estimateLowCredits"]
|
|
1971
|
+
}).refine((q) => q.approvedCapCredits >= q.estimateHighCredits, {
|
|
1972
|
+
message: "approvedCapCredits must be at least estimateHighCredits",
|
|
1973
|
+
path: ["approvedCapCredits"]
|
|
1974
|
+
}).refine((q) => q.approvedBy === null === (q.approvedAt === null), {
|
|
1975
|
+
message: "approvedBy and approvedAt are set together or not at all",
|
|
1976
|
+
path: ["approvedAt"]
|
|
1977
|
+
});
|
|
1895
1978
|
}
|
|
1896
1979
|
});
|
|
1897
1980
|
|
|
@@ -2495,9 +2578,10 @@ var init_src = __esm({
|
|
|
2495
2578
|
init_src();
|
|
2496
2579
|
import { createInterface } from "node:readline/promises";
|
|
2497
2580
|
import { spawn as spawn2 } from "node:child_process";
|
|
2498
|
-
import {
|
|
2581
|
+
import { mkdir as mkdir4 } from "node:fs/promises";
|
|
2582
|
+
import { accessSync, constants as constants2, existsSync as existsSync2, openSync, readFileSync, unlinkSync } from "node:fs";
|
|
2499
2583
|
import { hostname } from "node:os";
|
|
2500
|
-
import { delimiter, join as join5 } from "node:path";
|
|
2584
|
+
import { delimiter, dirname as dirname4, join as join5 } from "node:path";
|
|
2501
2585
|
|
|
2502
2586
|
// src/config/credentials.ts
|
|
2503
2587
|
import { chmod, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
@@ -2666,7 +2750,7 @@ function alive(pid) {
|
|
|
2666
2750
|
}
|
|
2667
2751
|
|
|
2668
2752
|
// src/version.ts
|
|
2669
|
-
var RUNNER_VERSION = "0.2.
|
|
2753
|
+
var RUNNER_VERSION = "0.2.5";
|
|
2670
2754
|
var RUNNER_VERSION_HEADER = "x-aloud-runner-version";
|
|
2671
2755
|
|
|
2672
2756
|
// src/protocol/client.ts
|
|
@@ -3988,6 +4072,7 @@ var ParticipantSession = class {
|
|
|
3988
4072
|
const startedAt = this.deps.clock.nowIso();
|
|
3989
4073
|
this.startedAtMs = this.deps.clock.monotonicMs();
|
|
3990
4074
|
let status2 = "active";
|
|
4075
|
+
let endReason = "error";
|
|
3991
4076
|
let error = null;
|
|
3992
4077
|
try {
|
|
3993
4078
|
this.emit({ type: "session.started", sessionId: this.input.sessionId, persona: this.persona.name });
|
|
@@ -3995,12 +4080,14 @@ var ParticipantSession = class {
|
|
|
3995
4080
|
this.lastCapture = first;
|
|
3996
4081
|
this.visited.push(first.digest);
|
|
3997
4082
|
await this.recordFirstImpression(first);
|
|
3998
|
-
status2 = await this.loop();
|
|
4083
|
+
({ status: status2, endReason } = await this.loop());
|
|
3999
4084
|
} catch (thrown) {
|
|
4000
4085
|
if (this.deps.signal?.aborted) {
|
|
4001
4086
|
status2 = "canceled";
|
|
4087
|
+
endReason = "canceled";
|
|
4002
4088
|
} else {
|
|
4003
4089
|
status2 = "error";
|
|
4090
|
+
endReason = "error";
|
|
4004
4091
|
const modelError = thrown instanceof ModelError ? thrown : null;
|
|
4005
4092
|
error = {
|
|
4006
4093
|
kind: modelError ? `model:${modelError.kind}` : "browser",
|
|
@@ -4015,6 +4102,7 @@ var ParticipantSession = class {
|
|
|
4015
4102
|
return {
|
|
4016
4103
|
sessionId: this.input.sessionId,
|
|
4017
4104
|
status: status2,
|
|
4105
|
+
endReason,
|
|
4018
4106
|
moments: this.moments,
|
|
4019
4107
|
evidenceAssets: this.evidenceAssets,
|
|
4020
4108
|
selfReportedOutcome: this.selfReportedOutcome,
|
|
@@ -4037,12 +4125,23 @@ var ParticipantSession = class {
|
|
|
4037
4125
|
throw error;
|
|
4038
4126
|
}
|
|
4039
4127
|
}
|
|
4128
|
+
/**
|
|
4129
|
+
* Returns both how the session ended and why.
|
|
4130
|
+
*
|
|
4131
|
+
* Both, rather than the status alone, because the status cannot carry the why: three of the exits
|
|
4132
|
+
* below are all `partial`, and MONETIZATION "Refunds" bills two of them and refunds the third
|
|
4133
|
+
* whole. This function is the only place in the system that knows which one was taken, so the
|
|
4134
|
+
* reason leaves here as data rather than being reconstructed later from a moment count that reads
|
|
4135
|
+
* identically either way.
|
|
4136
|
+
*/
|
|
4040
4137
|
async loop() {
|
|
4041
4138
|
const { limits } = this.input;
|
|
4042
4139
|
for (let step = 0; step < limits.maxActions; step += 1) {
|
|
4043
4140
|
this.checkCancelled();
|
|
4044
|
-
if (this.elapsedMs() > limits.maxElapsedMs) return "partial";
|
|
4045
|
-
if (this.deps.gateway.totalUsage().outputTokens > limits.maxTokens)
|
|
4141
|
+
if (this.elapsedMs() > limits.maxElapsedMs) return { status: "partial", endReason: "cap_elapsed" };
|
|
4142
|
+
if (this.deps.gateway.totalUsage().outputTokens > limits.maxTokens) {
|
|
4143
|
+
return { status: "partial", endReason: "cap_tokens" };
|
|
4144
|
+
}
|
|
4046
4145
|
const capture = this.lastCapture;
|
|
4047
4146
|
const momentId = newId("moment");
|
|
4048
4147
|
const decision = await this.decide(capture, momentId);
|
|
@@ -4058,12 +4157,12 @@ var ParticipantSession = class {
|
|
|
4058
4157
|
confidence: decision.confidence,
|
|
4059
4158
|
capture
|
|
4060
4159
|
});
|
|
4061
|
-
return decision.action.outcome === "abandoned" ? "abandoned" : "partial";
|
|
4160
|
+
return decision.action.outcome === "abandoned" ? { status: "abandoned", endReason: "abandoned" } : { status: "partial", endReason: "finished" };
|
|
4062
4161
|
}
|
|
4063
4162
|
const outcome = await this.act(decision, capture, momentId);
|
|
4064
|
-
if (outcome === "stop") return "abandoned";
|
|
4163
|
+
if (outcome === "stop") return { status: "abandoned", endReason: "abandoned" };
|
|
4065
4164
|
}
|
|
4066
|
-
return "partial";
|
|
4165
|
+
return { status: "partial", endReason: "cap_actions" };
|
|
4067
4166
|
}
|
|
4068
4167
|
async decide(capture, momentId) {
|
|
4069
4168
|
const result = await this.deps.gateway.generate({
|
|
@@ -5022,6 +5121,11 @@ var RunEventLog = class {
|
|
|
5022
5121
|
|
|
5023
5122
|
// ../engine/src/orchestrator/coordinator.ts
|
|
5024
5123
|
init_src();
|
|
5124
|
+
var DEFAULT_SESSION_LIMITS = {
|
|
5125
|
+
maxActions: 40,
|
|
5126
|
+
maxElapsedMs: 15 * 6e4,
|
|
5127
|
+
maxTokens: 4e5
|
|
5128
|
+
};
|
|
5025
5129
|
var RunCoordinator = class {
|
|
5026
5130
|
constructor(input, deps) {
|
|
5027
5131
|
this.input = input;
|
|
@@ -5160,6 +5264,7 @@ var RunCoordinator = class {
|
|
|
5160
5264
|
*/
|
|
5161
5265
|
async runSessions(budgets) {
|
|
5162
5266
|
const { snapshot } = this.input;
|
|
5267
|
+
const limits = snapshot.sessionLimits ?? DEFAULT_SESSION_LIMITS;
|
|
5163
5268
|
const limit = Math.max(1, this.deps.maxConcurrentSessions ?? snapshot.cast.length);
|
|
5164
5269
|
const results = new Array(snapshot.cast.length);
|
|
5165
5270
|
let cursor = 0;
|
|
@@ -5181,7 +5286,7 @@ var RunCoordinator = class {
|
|
|
5181
5286
|
persona,
|
|
5182
5287
|
snapshot,
|
|
5183
5288
|
patienceBudget: budgets[index],
|
|
5184
|
-
limits
|
|
5289
|
+
limits
|
|
5185
5290
|
},
|
|
5186
5291
|
{
|
|
5187
5292
|
gateway: this.deps.gateway,
|
|
@@ -5223,12 +5328,17 @@ var RunCoordinator = class {
|
|
|
5223
5328
|
selfReportedReason: result.selfReportedReason,
|
|
5224
5329
|
patienceBudget: budgets[index],
|
|
5225
5330
|
patienceState: result.patienceState,
|
|
5226
|
-
limits
|
|
5331
|
+
limits,
|
|
5227
5332
|
startedAt: result.startedAt,
|
|
5228
5333
|
endedAt: result.endedAt,
|
|
5229
5334
|
error: result.error ? { kind: "internal", message: result.error.message, retryable: result.error.retryable, occurredAt: result.endedAt } : null,
|
|
5230
5335
|
contamination: null,
|
|
5231
5336
|
momentCount: result.moments.length,
|
|
5337
|
+
// Carried straight from the loop rather than derived from `status`, which cannot say: a
|
|
5338
|
+
// clean finish and an exhausted action budget are both `partial`. MONETIZATION "Refunds"
|
|
5339
|
+
// bills the first and refunds the second whole, so the distinction has to survive the trip
|
|
5340
|
+
// into the persisted record.
|
|
5341
|
+
endReason: result.endReason,
|
|
5232
5342
|
workerId: worker.id
|
|
5233
5343
|
};
|
|
5234
5344
|
this.sessionsByPersonaIndex.set(index, session);
|
|
@@ -6246,6 +6356,9 @@ function failureFrom(outcome) {
|
|
|
6246
6356
|
}
|
|
6247
6357
|
function describeOutcome(result) {
|
|
6248
6358
|
if (result.status === "error") return "the browser stopped unexpectedly";
|
|
6359
|
+
if (result.endReason === "cap_actions") return "ran out of actions before finishing";
|
|
6360
|
+
if (result.endReason === "cap_elapsed") return "ran out of time before finishing";
|
|
6361
|
+
if (result.endReason === "cap_tokens") return "ran out of its token budget before finishing";
|
|
6249
6362
|
if (result.selfReportedOutcome === "success") return "reached the goal";
|
|
6250
6363
|
if (result.selfReportedOutcome === "abandoned") return result.selfReportedReason ?? "gave up";
|
|
6251
6364
|
return result.selfReportedOutcome ?? "finished";
|
|
@@ -6639,6 +6752,10 @@ var StudyDesignProposalResponse = z23.object({
|
|
|
6639
6752
|
// ../app/src/services/billing.ts
|
|
6640
6753
|
init_src();
|
|
6641
6754
|
|
|
6755
|
+
// ../app/src/services/billing-outbox.ts
|
|
6756
|
+
init_src();
|
|
6757
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
6758
|
+
|
|
6642
6759
|
// ../app/src/application.ts
|
|
6643
6760
|
var PreflightFailedError = class extends Error {
|
|
6644
6761
|
constructor(preflight2) {
|
|
@@ -6651,6 +6768,9 @@ var PreflightFailedError = class extends Error {
|
|
|
6651
6768
|
preflight;
|
|
6652
6769
|
};
|
|
6653
6770
|
|
|
6771
|
+
// ../app/src/services/billing-webhooks.ts
|
|
6772
|
+
init_src();
|
|
6773
|
+
|
|
6654
6774
|
// ../app/src/services/export.ts
|
|
6655
6775
|
var DEFAULT_MAX_EMBEDDED_BYTES = 24 * 1024 * 1024;
|
|
6656
6776
|
|
|
@@ -6875,6 +6995,8 @@ var MCP_TOOLS = [
|
|
|
6875
6995
|
"propose_personas",
|
|
6876
6996
|
"approve_personas",
|
|
6877
6997
|
"run_preflight",
|
|
6998
|
+
"quote_study",
|
|
6999
|
+
"approve_quote",
|
|
6878
7000
|
"start_study",
|
|
6879
7001
|
"get_run_status",
|
|
6880
7002
|
"cancel_run",
|
|
@@ -6890,6 +7012,12 @@ var MUTATING_TOOLS = {
|
|
|
6890
7012
|
propose_personas: { mutates: false, capability: "study.write", idempotent: true },
|
|
6891
7013
|
approve_personas: { mutates: true, capability: "study.write", idempotent: true },
|
|
6892
7014
|
run_preflight: { mutates: false, capability: "study.write", idempotent: true },
|
|
7015
|
+
// BILLING_PLAN 3. Quoting is a builder operation and changes no money, so it is authorised the
|
|
7016
|
+
// same way `run_preflight` is. Approving the ceiling is what commits a run to spend, so it is
|
|
7017
|
+
// authorised as `run.start`: an operator can start runs and cannot manage billing, and requiring
|
|
7018
|
+
// `billing.manage` here would mean no operator could ever start a study.
|
|
7019
|
+
quote_study: { mutates: true, capability: "study.write", idempotent: false },
|
|
7020
|
+
approve_quote: { mutates: true, capability: "run.start", idempotent: true },
|
|
6893
7021
|
start_study: { mutates: true, capability: "run.start", idempotent: false },
|
|
6894
7022
|
get_run_status: { mutates: false, capability: "report.read", idempotent: true },
|
|
6895
7023
|
cancel_run: { mutates: true, capability: "run.cancel", idempotent: true },
|
|
@@ -6937,8 +7065,25 @@ var StartStudyInput = z25.object({
|
|
|
6937
7065
|
productId: z25.string(),
|
|
6938
7066
|
/** SPEC FR-116: supplied by the client so a retry finds the run rather than making a second. */
|
|
6939
7067
|
idempotencyKey: z25.string().min(1).max(120),
|
|
6940
|
-
maxCostCents: z25.number().positive().nullable().default(null)
|
|
7068
|
+
maxCostCents: z25.number().positive().nullable().default(null),
|
|
7069
|
+
/**
|
|
7070
|
+
* BILLING_PLAN 3: the run `quote_study` priced and `approve_quote` authorised.
|
|
7071
|
+
*
|
|
7072
|
+
* Optional in the schema and required in effect: a start with no run id gets a fresh one, which
|
|
7073
|
+
* by construction carries no approved quote, and `@aloud/app` refuses it. Typed as optional so
|
|
7074
|
+
* the refusal comes from the one authorization path rather than from an argument parser here,
|
|
7075
|
+
* which is SPEC FR-110: two enforcement points is one too many.
|
|
7076
|
+
*/
|
|
7077
|
+
runId: z25.string().optional()
|
|
6941
7078
|
});
|
|
7079
|
+
var QuoteStudyInput = z25.object({
|
|
7080
|
+
studyId: z25.string(),
|
|
7081
|
+
/** Re-quotes an existing prospective run instead of pricing a new one. */
|
|
7082
|
+
runId: z25.string().optional(),
|
|
7083
|
+
/** Overrides the archetype prediction of moments per participant. */
|
|
7084
|
+
momentsEach: z25.number().int().min(1).max(400).optional()
|
|
7085
|
+
});
|
|
7086
|
+
var ApproveQuoteInput = z25.object({ runId: z25.string() });
|
|
6942
7087
|
var RunIdInput = z25.object({ runId: z25.string() });
|
|
6943
7088
|
var FindingInput = z25.object({ runId: z25.string(), findingId: z25.string() });
|
|
6944
7089
|
var ShareLinkInput = z25.object({
|
|
@@ -7025,7 +7170,7 @@ async function handleTool(context, name, input, execute) {
|
|
|
7025
7170
|
castComposition: draft.cast.compositionFloor,
|
|
7026
7171
|
preflight: summarisePreflight(draft.preflight),
|
|
7027
7172
|
estimateCents: app.estimateFor(draft.study),
|
|
7028
|
-
note: "Nothing has run yet. Approve the cast, then start the
|
|
7173
|
+
note: "Nothing has run yet. Approve the cast, quote the study, get the ceiling approved, then start the run the quote named."
|
|
7029
7174
|
});
|
|
7030
7175
|
}
|
|
7031
7176
|
case "get_product_profile": {
|
|
@@ -7077,6 +7222,50 @@ async function handleTool(context, name, input, execute) {
|
|
|
7077
7222
|
const preflight2 = await app.preflight(actor, { workspaceId, studyId: parsed.studyId });
|
|
7078
7223
|
return json(summarisePreflight(preflight2));
|
|
7079
7224
|
}
|
|
7225
|
+
/**
|
|
7226
|
+
* BILLING_PLAN 3: "The MCP path enforces the same gate as the web path, through the one
|
|
7227
|
+
* authorization path in `@aloud/app`."
|
|
7228
|
+
*
|
|
7229
|
+
* That sentence is why this is three calls rather than one convenient one. An agent that could
|
|
7230
|
+
* start a study without a recorded agreement would be a way around the gate, and the whole
|
|
7231
|
+
* point of the gate is that a variable meter needs an agreement somebody actually made. So the
|
|
7232
|
+
* agent quotes, shows the human the range, gets the ceiling approved, and starts the run the
|
|
7233
|
+
* quote named.
|
|
7234
|
+
*/
|
|
7235
|
+
case "quote_study": {
|
|
7236
|
+
const parsed = QuoteStudyInput.parse(input);
|
|
7237
|
+
const quoted = await app.quoteStudy(actor, { workspaceId, ...parsed });
|
|
7238
|
+
return json({
|
|
7239
|
+
quoteId: quoted.quote.id,
|
|
7240
|
+
// Carried into approve_quote and start_study. The agreement names the run, so the run has
|
|
7241
|
+
// to be named before it exists.
|
|
7242
|
+
runId: quoted.quote.runId,
|
|
7243
|
+
resource: RESOURCE_SCHEMES.run(quoted.quote.runId),
|
|
7244
|
+
rateCardVersion: quoted.quote.rateCardVersion,
|
|
7245
|
+
estimateLowCredits: quoted.quote.estimateLowCredits,
|
|
7246
|
+
estimateHighCredits: quoted.quote.estimateHighCredits,
|
|
7247
|
+
approvedCapCredits: quoted.quote.approvedCapCredits,
|
|
7248
|
+
// MONETIZATION.md, "Quoting a study": the range is what a customer is promised and the
|
|
7249
|
+
// basis is how much that promise is worth, so the two travel together.
|
|
7250
|
+
basis: quoted.basis,
|
|
7251
|
+
observations: quoted.observations,
|
|
7252
|
+
approved: false,
|
|
7253
|
+
note: "Nothing is committed. Moment count is not knowable in advance, so this is a range and a ceiling rather than a price. Show it to a person, then call approve_quote with this runId, then start_study with the same runId."
|
|
7254
|
+
});
|
|
7255
|
+
}
|
|
7256
|
+
case "approve_quote": {
|
|
7257
|
+
const parsed = ApproveQuoteInput.parse(input);
|
|
7258
|
+
const quote = await app.approveQuote(actor, { workspaceId, runId: parsed.runId });
|
|
7259
|
+
return json({
|
|
7260
|
+
quoteId: quote.id,
|
|
7261
|
+
runId: quote.runId,
|
|
7262
|
+
approvedCapCredits: quote.approvedCapCredits,
|
|
7263
|
+
approvedBy: quote.approvedBy,
|
|
7264
|
+
approvedAt: quote.approvedAt,
|
|
7265
|
+
rateCardVersion: quote.rateCardVersion,
|
|
7266
|
+
note: "This run may never spend more than the approved ceiling. It becomes the per-session action limit, so the cost control and the honest-abandonment guarantee are one mechanism."
|
|
7267
|
+
});
|
|
7268
|
+
}
|
|
7080
7269
|
case "start_study": {
|
|
7081
7270
|
const parsed = StartStudyInput.parse(input);
|
|
7082
7271
|
let outcome;
|
|
@@ -7085,7 +7274,10 @@ async function handleTool(context, name, input, execute) {
|
|
|
7085
7274
|
workspaceId,
|
|
7086
7275
|
studyId: parsed.studyId,
|
|
7087
7276
|
idempotencyKey: parsed.idempotencyKey,
|
|
7088
|
-
maxCostCents: parsed.maxCostCents
|
|
7277
|
+
maxCostCents: parsed.maxCostCents,
|
|
7278
|
+
// BILLING_PLAN 3: the run the approved quote priced. Omitted, the gate in `@aloud/app`
|
|
7279
|
+
// refuses, which is the same refusal the web path gets.
|
|
7280
|
+
...parsed.runId ? { runId: parsed.runId } : {}
|
|
7089
7281
|
});
|
|
7090
7282
|
} catch (error) {
|
|
7091
7283
|
if (error instanceof PreflightFailedError) {
|
|
@@ -7101,13 +7293,13 @@ async function handleTool(context, name, input, execute) {
|
|
|
7101
7293
|
}
|
|
7102
7294
|
throw error;
|
|
7103
7295
|
}
|
|
7104
|
-
const { run, started, preflight: preflight2 } = outcome;
|
|
7105
|
-
await execute?.(
|
|
7296
|
+
const { run: run2, started, preflight: preflight2 } = outcome;
|
|
7297
|
+
await execute?.(run2.id, parsed.productId, started);
|
|
7106
7298
|
return json({
|
|
7107
|
-
runId:
|
|
7108
|
-
resource: RESOURCE_SCHEMES.run(
|
|
7109
|
-
webUrl: context.webBaseUrl ? `${context.webBaseUrl}/app/runs/${
|
|
7110
|
-
status:
|
|
7299
|
+
runId: run2.id,
|
|
7300
|
+
resource: RESOURCE_SCHEMES.run(run2.id),
|
|
7301
|
+
webUrl: context.webBaseUrl ? `${context.webBaseUrl}/app/runs/${run2.id}` : null,
|
|
7302
|
+
status: run2.status,
|
|
7111
7303
|
// SPEC FR-116: a retry with the same key reports that it found the existing run.
|
|
7112
7304
|
alreadyRunning: !started,
|
|
7113
7305
|
preflight: preflight2 ? summarisePreflight(preflight2) : null,
|
|
@@ -7135,8 +7327,8 @@ async function handleTool(context, name, input, execute) {
|
|
|
7135
7327
|
}
|
|
7136
7328
|
case "cancel_run": {
|
|
7137
7329
|
const parsed = z25.object({ runId: z25.string(), reason: z25.string().optional() }).parse(input);
|
|
7138
|
-
const
|
|
7139
|
-
return json({ runId:
|
|
7330
|
+
const run2 = await app.cancelRun(actor, { workspaceId, ...parsed });
|
|
7331
|
+
return json({ runId: run2.id, status: run2.status });
|
|
7140
7332
|
}
|
|
7141
7333
|
case "list_findings": {
|
|
7142
7334
|
const parsed = RunIdInput.parse(input);
|
|
@@ -7238,7 +7430,9 @@ var TOOL_DESCRIPTIONS = {
|
|
|
7238
7430
|
propose_personas: "Propose a cast of behaviourally distinct participants for an existing study, derived only from what a visitor to the site could see.",
|
|
7239
7431
|
approve_personas: "Approve the cast a study will run with. Required before a study can start.",
|
|
7240
7432
|
run_preflight: "Check a study is safe and ready to run. Names the exact failing check rather than a generic failure.",
|
|
7241
|
-
|
|
7433
|
+
quote_study: "Price a study before it runs. Moment count is not knowable in advance, so this returns an estimate range and a ceiling rather than a price, plus the runId the quote is recorded against. Show the range to a person before approving it.",
|
|
7434
|
+
approve_quote: "Record that the operator agreed to a study's ceiling. Required before the run can start, and it becomes the per-session action limit so the run can never exceed what was approved.",
|
|
7435
|
+
start_study: "Start a study. Returns a run id immediately; the study runs in the background. Requires an idempotency key so a retry finds the existing run instead of starting a second, and the runId from an approved quote.",
|
|
7242
7436
|
get_run_status: "Read a run's current status, its participants, and its report if one exists yet.",
|
|
7243
7437
|
cancel_run: "Stop a run. Browsers are closed and no further model spend happens.",
|
|
7244
7438
|
list_findings: "List the findings from a completed run, with the participant count behind each one and the study's stated limitations.",
|
|
@@ -7299,11 +7493,18 @@ var TOOL_SHAPES = {
|
|
|
7299
7493
|
},
|
|
7300
7494
|
approve_personas: { studyId: z26.string(), cast: z26.array(z26.record(z26.string(), z26.unknown())) },
|
|
7301
7495
|
run_preflight: { studyId: z26.string() },
|
|
7496
|
+
quote_study: {
|
|
7497
|
+
studyId: z26.string(),
|
|
7498
|
+
runId: z26.string().describe("Re-quote a prospective run instead of pricing a new one.").optional(),
|
|
7499
|
+
momentsEach: z26.number().int().min(1).max(400).describe("Override the predicted moments per participant.").optional()
|
|
7500
|
+
},
|
|
7501
|
+
approve_quote: { runId: z26.string().describe("The runId quote_study returned.") },
|
|
7302
7502
|
start_study: {
|
|
7303
7503
|
studyId: z26.string(),
|
|
7304
7504
|
productId: z26.string(),
|
|
7305
7505
|
idempotencyKey: z26.string().describe("Reuse this on a retry so it finds the run rather than starting a second."),
|
|
7306
|
-
maxCostCents: z26.number().positive().nullable().optional()
|
|
7506
|
+
maxCostCents: z26.number().positive().nullable().optional(),
|
|
7507
|
+
runId: z26.string().describe("The runId from the approved quote. A run cannot start without one.").optional()
|
|
7307
7508
|
},
|
|
7308
7509
|
get_run_status: { runId: z26.string() },
|
|
7309
7510
|
cancel_run: { runId: z26.string(), reason: z26.string().optional() },
|
|
@@ -7328,6 +7529,11 @@ function createMcpServer(options) {
|
|
|
7328
7529
|
"not reports from real customers, and every response says how many participants hit a thing",
|
|
7329
7530
|
"rather than what share of users would.",
|
|
7330
7531
|
"",
|
|
7532
|
+
"A study costs money to run, so it cannot start until somebody has agreed to a ceiling.",
|
|
7533
|
+
"Call quote_study, show a person the range and the cap it returns, call approve_quote, then",
|
|
7534
|
+
"start_study with the same runId. The estimate is a range because how far a participant gets",
|
|
7535
|
+
"is the thing being measured, so there is no single price to quote.",
|
|
7536
|
+
"",
|
|
7331
7537
|
"start_study returns immediately. Poll get_run_status; nothing is lost if this connection drops.",
|
|
7332
7538
|
"",
|
|
7333
7539
|
"When you are running beside a product repository, call upsert_product_context with derived",
|
|
@@ -7494,7 +7700,7 @@ async function readResource(context, parsed) {
|
|
|
7494
7700
|
name: study.name,
|
|
7495
7701
|
goal: study.goal,
|
|
7496
7702
|
status: study.status,
|
|
7497
|
-
runs: runs.map((
|
|
7703
|
+
runs: runs.map((run2) => ({ runId: run2.id, status: run2.status, resource: RESOURCE_SCHEMES.run(run2.id) }))
|
|
7498
7704
|
},
|
|
7499
7705
|
null,
|
|
7500
7706
|
2
|
|
@@ -7737,32 +7943,47 @@ async function setup() {
|
|
|
7737
7943
|
const installed = onPath("aloud");
|
|
7738
7944
|
const latest = await latestVersion();
|
|
7739
7945
|
const stale = latest !== null && latest !== RUNNER_VERSION;
|
|
7946
|
+
const signedIn = await signedInState(credentials);
|
|
7740
7947
|
const out = (line = "") => process.stdout.write(line + "\n");
|
|
7741
7948
|
out();
|
|
7742
7949
|
out("Aloud runner setup. You are looking at the state of this machine.");
|
|
7743
7950
|
out();
|
|
7744
7951
|
out(` installed ${installed ? `yes (${RUNNER_VERSION})` : "no"}`);
|
|
7745
7952
|
out(` up to date ${latest === null ? "unknown, could not reach the registry" : stale ? `no, ${latest} is out` : "yes"}`);
|
|
7746
|
-
out(
|
|
7953
|
+
out(
|
|
7954
|
+
` signed in ${signedIn.state === "ok" ? signedIn.name : signedIn.state === "revoked" ? `no. The saved token for ${credentials?.runnerName ?? "this machine"} was revoked` : signedIn.state === "unreachable" ? `cannot tell, ${signedIn.server} did not answer` : "no"}`
|
|
7955
|
+
);
|
|
7747
7956
|
out(` chromium ${checks.chromiumInstalled ? "ready" : "downloads on first start, about 350 MB"}`);
|
|
7748
7957
|
out(` running ${running ? `yes (pid ${running.pid})` : "no"}`);
|
|
7749
7958
|
out();
|
|
7959
|
+
if (process.stdin.isTTY) return interactiveSetup({ installed, stale, latest, signedIn, running, credentials });
|
|
7750
7960
|
const steps = [];
|
|
7751
7961
|
if (!installed) {
|
|
7752
|
-
steps.push("npm install -g @aloud/runner");
|
|
7962
|
+
steps.push(["npm install -g @aloud/runner"]);
|
|
7753
7963
|
} else if (stale) {
|
|
7754
|
-
steps.push(`npm install -g @aloud/runner@latest
|
|
7964
|
+
steps.push([`npm install -g @aloud/runner@latest`, `${RUNNER_VERSION} is installed, ${latest} is out.`]);
|
|
7755
7965
|
}
|
|
7756
|
-
if (
|
|
7757
|
-
steps.push(
|
|
7758
|
-
|
|
7759
|
-
|
|
7760
|
-
|
|
7761
|
-
|
|
7762
|
-
|
|
7966
|
+
if (signedIn.state === "revoked") {
|
|
7967
|
+
steps.push([
|
|
7968
|
+
`Create a new token at ${credentials?.server ?? DEFAULT_SERVER}/app/settings/runners`,
|
|
7969
|
+
"The saved one was revoked and cannot be reused."
|
|
7970
|
+
]);
|
|
7971
|
+
}
|
|
7972
|
+
if (signedIn.state === "none" || signedIn.state === "revoked") {
|
|
7973
|
+
steps.push([
|
|
7974
|
+
"aloud login",
|
|
7975
|
+
"Needs a terminal. If you are an agent, hand this step to the person: they run it",
|
|
7976
|
+
"themselves and paste the token at the prompt. Do not ask them to paste it to you."
|
|
7977
|
+
]);
|
|
7763
7978
|
}
|
|
7764
7979
|
if (!running) {
|
|
7765
|
-
steps.push("aloud start
|
|
7980
|
+
steps.push(["aloud start", "In a background shell. It never exits, so do not block on it."]);
|
|
7981
|
+
}
|
|
7982
|
+
if (steps.length === 0 && signedIn.state === "unreachable") {
|
|
7983
|
+
out(`Cannot reach ${signedIn.server}, so there is nothing useful to say about what is left.`);
|
|
7984
|
+
out("Check the connection and run this again.");
|
|
7985
|
+
out();
|
|
7986
|
+
return 1;
|
|
7766
7987
|
}
|
|
7767
7988
|
if (steps.length === 0) {
|
|
7768
7989
|
out("Nothing to do. This machine is set up and waiting for studies.");
|
|
@@ -7771,8 +7992,12 @@ async function setup() {
|
|
|
7771
7992
|
}
|
|
7772
7993
|
out("Do these, in order:");
|
|
7773
7994
|
out();
|
|
7774
|
-
for (const [index,
|
|
7775
|
-
|
|
7995
|
+
for (const [index, [command, ...notes]] of steps.entries()) {
|
|
7996
|
+
out(` ${index + 1}. ${command}`);
|
|
7997
|
+
for (const note of notes) out(` ${note}`);
|
|
7998
|
+
}
|
|
7999
|
+
out(` ${steps.length + 1}. aloud status`);
|
|
8000
|
+
out(" Exits non-zero until all of the above are true.");
|
|
7776
8001
|
out();
|
|
7777
8002
|
out("Rules, if you are an agent doing this:");
|
|
7778
8003
|
out();
|
|
@@ -7796,6 +8021,105 @@ async function setup() {
|
|
|
7796
8021
|
}
|
|
7797
8022
|
return 1;
|
|
7798
8023
|
}
|
|
8024
|
+
async function interactiveSetup(state) {
|
|
8025
|
+
const out = (line = "") => process.stdout.write(line + "\n");
|
|
8026
|
+
const server = state.credentials?.server ?? DEFAULT_SERVER;
|
|
8027
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
8028
|
+
try {
|
|
8029
|
+
if (!state.installed || state.stale) {
|
|
8030
|
+
const what = state.installed ? `Update to ${state.latest}` : "Install it globally";
|
|
8031
|
+
if (await confirm(rl, `${what} with npm?`)) {
|
|
8032
|
+
const ok = await run("npm", ["install", "-g", "@aloud/runner@latest"], out);
|
|
8033
|
+
if (!ok) {
|
|
8034
|
+
out("");
|
|
8035
|
+
out("That install did not work. If it asked for permissions, do not use sudo:");
|
|
8036
|
+
out("npm's global prefix belongs to you or it does not, and sudo papers over the wrong one.");
|
|
8037
|
+
return 1;
|
|
8038
|
+
}
|
|
8039
|
+
}
|
|
8040
|
+
}
|
|
8041
|
+
if (state.signedIn.state !== "ok") {
|
|
8042
|
+
out("");
|
|
8043
|
+
if (state.signedIn.state === "revoked") {
|
|
8044
|
+
out(`The token saved here was revoked, so this machine needs a new one.`);
|
|
8045
|
+
}
|
|
8046
|
+
out(`Create a token at ${server}/app/settings/runners`);
|
|
8047
|
+
out("It is shown once. Copy it, then paste it below.");
|
|
8048
|
+
out("");
|
|
8049
|
+
const token = (await rl.question("Token: ")).trim();
|
|
8050
|
+
rl.close();
|
|
8051
|
+
const code = await login(["--token", token, "--server", server]);
|
|
8052
|
+
if (code !== 0) return code;
|
|
8053
|
+
} else {
|
|
8054
|
+
rl.close();
|
|
8055
|
+
}
|
|
8056
|
+
if (!state.running) {
|
|
8057
|
+
const second = createInterface({ input: process.stdin, output: process.stdout });
|
|
8058
|
+
const start2 = await confirm(second, "Start the runner now, in the background?");
|
|
8059
|
+
second.close();
|
|
8060
|
+
if (!start2) {
|
|
8061
|
+
out("");
|
|
8062
|
+
out("Start it when you are ready, and leave it running: aloud start");
|
|
8063
|
+
return 1;
|
|
8064
|
+
}
|
|
8065
|
+
return startDetached(out);
|
|
8066
|
+
}
|
|
8067
|
+
out("");
|
|
8068
|
+
out("Set up. This machine is waiting for studies.");
|
|
8069
|
+
out("");
|
|
8070
|
+
return 0;
|
|
8071
|
+
} finally {
|
|
8072
|
+
rl.close();
|
|
8073
|
+
}
|
|
8074
|
+
}
|
|
8075
|
+
async function confirm(rl, question) {
|
|
8076
|
+
const answer = (await rl.question(`${question} [Y/n] `)).trim().toLowerCase();
|
|
8077
|
+
return answer === "" || answer === "y" || answer === "yes";
|
|
8078
|
+
}
|
|
8079
|
+
async function run(command, args, out) {
|
|
8080
|
+
out("");
|
|
8081
|
+
out(` ${command} ${args.join(" ")}`);
|
|
8082
|
+
return new Promise((resolve) => {
|
|
8083
|
+
const child = spawn2(command, [...args], { stdio: ["ignore", "pipe", "pipe"] });
|
|
8084
|
+
child.stdout?.on("data", (chunk) => out(" " + chunk.toString("utf8").trimEnd()));
|
|
8085
|
+
child.stderr?.on("data", (chunk) => out(" " + chunk.toString("utf8").trimEnd()));
|
|
8086
|
+
child.on("error", () => resolve(false));
|
|
8087
|
+
child.on("close", (code) => resolve(code === 0));
|
|
8088
|
+
});
|
|
8089
|
+
}
|
|
8090
|
+
async function startDetached(out) {
|
|
8091
|
+
const log = join5(dirname4(credentialsPath()), "runner.log");
|
|
8092
|
+
await mkdir4(dirname4(log), { recursive: true, mode: 448 });
|
|
8093
|
+
const handle = openSync(log, "a");
|
|
8094
|
+
const child = spawn2(process.execPath, [process.argv[1] ?? "", "start"], {
|
|
8095
|
+
detached: true,
|
|
8096
|
+
stdio: ["ignore", handle, handle]
|
|
8097
|
+
});
|
|
8098
|
+
child.unref();
|
|
8099
|
+
out("");
|
|
8100
|
+
out(`Started in the background, pid ${child.pid}.`);
|
|
8101
|
+
out(` Output ${log}`);
|
|
8102
|
+
out(" Check it aloud status");
|
|
8103
|
+
out(` Stop it kill ${child.pid}`);
|
|
8104
|
+
out("");
|
|
8105
|
+
out("The first start downloads Chromium, about 350 MB, once. Studies will wait until it is done.");
|
|
8106
|
+
out("");
|
|
8107
|
+
return 0;
|
|
8108
|
+
}
|
|
8109
|
+
async function signedInState(credentials) {
|
|
8110
|
+
if (!credentials) return { state: "none" };
|
|
8111
|
+
try {
|
|
8112
|
+
const response = await fetch(new URL("api/runner/me", credentials.server + "/"), {
|
|
8113
|
+
headers: { authorization: `Bearer ${credentials.token}` },
|
|
8114
|
+
signal: AbortSignal.timeout(5e3)
|
|
8115
|
+
});
|
|
8116
|
+
if (response.status === 401 || response.status === 403) return { state: "revoked" };
|
|
8117
|
+
if (!response.ok) return { state: "unreachable", server: credentials.server };
|
|
8118
|
+
return { state: "ok", name: credentials.runnerName };
|
|
8119
|
+
} catch {
|
|
8120
|
+
return { state: "unreachable", server: credentials.server };
|
|
8121
|
+
}
|
|
8122
|
+
}
|
|
7799
8123
|
async function npmPrefix() {
|
|
7800
8124
|
const path = await new Promise((resolve) => {
|
|
7801
8125
|
const child = spawn2("npm", ["config", "get", "prefix"], { stdio: ["ignore", "pipe", "ignore"] });
|