@aloud/runner 0.2.3 → 0.2.4
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 +450 -213
- package/package.json +1 -1
- package/src/cli.ts +70 -14
- 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
|
|
|
@@ -2666,7 +2749,7 @@ function alive(pid) {
|
|
|
2666
2749
|
}
|
|
2667
2750
|
|
|
2668
2751
|
// src/version.ts
|
|
2669
|
-
var RUNNER_VERSION = "0.2.
|
|
2752
|
+
var RUNNER_VERSION = "0.2.4";
|
|
2670
2753
|
var RUNNER_VERSION_HEADER = "x-aloud-runner-version";
|
|
2671
2754
|
|
|
2672
2755
|
// src/protocol/client.ts
|
|
@@ -3988,6 +4071,7 @@ var ParticipantSession = class {
|
|
|
3988
4071
|
const startedAt = this.deps.clock.nowIso();
|
|
3989
4072
|
this.startedAtMs = this.deps.clock.monotonicMs();
|
|
3990
4073
|
let status2 = "active";
|
|
4074
|
+
let endReason = "error";
|
|
3991
4075
|
let error = null;
|
|
3992
4076
|
try {
|
|
3993
4077
|
this.emit({ type: "session.started", sessionId: this.input.sessionId, persona: this.persona.name });
|
|
@@ -3995,12 +4079,14 @@ var ParticipantSession = class {
|
|
|
3995
4079
|
this.lastCapture = first;
|
|
3996
4080
|
this.visited.push(first.digest);
|
|
3997
4081
|
await this.recordFirstImpression(first);
|
|
3998
|
-
status2 = await this.loop();
|
|
4082
|
+
({ status: status2, endReason } = await this.loop());
|
|
3999
4083
|
} catch (thrown) {
|
|
4000
4084
|
if (this.deps.signal?.aborted) {
|
|
4001
4085
|
status2 = "canceled";
|
|
4086
|
+
endReason = "canceled";
|
|
4002
4087
|
} else {
|
|
4003
4088
|
status2 = "error";
|
|
4089
|
+
endReason = "error";
|
|
4004
4090
|
const modelError = thrown instanceof ModelError ? thrown : null;
|
|
4005
4091
|
error = {
|
|
4006
4092
|
kind: modelError ? `model:${modelError.kind}` : "browser",
|
|
@@ -4015,6 +4101,7 @@ var ParticipantSession = class {
|
|
|
4015
4101
|
return {
|
|
4016
4102
|
sessionId: this.input.sessionId,
|
|
4017
4103
|
status: status2,
|
|
4104
|
+
endReason,
|
|
4018
4105
|
moments: this.moments,
|
|
4019
4106
|
evidenceAssets: this.evidenceAssets,
|
|
4020
4107
|
selfReportedOutcome: this.selfReportedOutcome,
|
|
@@ -4037,12 +4124,23 @@ var ParticipantSession = class {
|
|
|
4037
4124
|
throw error;
|
|
4038
4125
|
}
|
|
4039
4126
|
}
|
|
4127
|
+
/**
|
|
4128
|
+
* Returns both how the session ended and why.
|
|
4129
|
+
*
|
|
4130
|
+
* Both, rather than the status alone, because the status cannot carry the why: three of the exits
|
|
4131
|
+
* below are all `partial`, and MONETIZATION "Refunds" bills two of them and refunds the third
|
|
4132
|
+
* whole. This function is the only place in the system that knows which one was taken, so the
|
|
4133
|
+
* reason leaves here as data rather than being reconstructed later from a moment count that reads
|
|
4134
|
+
* identically either way.
|
|
4135
|
+
*/
|
|
4040
4136
|
async loop() {
|
|
4041
4137
|
const { limits } = this.input;
|
|
4042
4138
|
for (let step = 0; step < limits.maxActions; step += 1) {
|
|
4043
4139
|
this.checkCancelled();
|
|
4044
|
-
if (this.elapsedMs() > limits.maxElapsedMs) return "partial";
|
|
4045
|
-
if (this.deps.gateway.totalUsage().outputTokens > limits.maxTokens)
|
|
4140
|
+
if (this.elapsedMs() > limits.maxElapsedMs) return { status: "partial", endReason: "cap_elapsed" };
|
|
4141
|
+
if (this.deps.gateway.totalUsage().outputTokens > limits.maxTokens) {
|
|
4142
|
+
return { status: "partial", endReason: "cap_tokens" };
|
|
4143
|
+
}
|
|
4046
4144
|
const capture = this.lastCapture;
|
|
4047
4145
|
const momentId = newId("moment");
|
|
4048
4146
|
const decision = await this.decide(capture, momentId);
|
|
@@ -4058,12 +4156,12 @@ var ParticipantSession = class {
|
|
|
4058
4156
|
confidence: decision.confidence,
|
|
4059
4157
|
capture
|
|
4060
4158
|
});
|
|
4061
|
-
return decision.action.outcome === "abandoned" ? "abandoned" : "partial";
|
|
4159
|
+
return decision.action.outcome === "abandoned" ? { status: "abandoned", endReason: "abandoned" } : { status: "partial", endReason: "finished" };
|
|
4062
4160
|
}
|
|
4063
4161
|
const outcome = await this.act(decision, capture, momentId);
|
|
4064
|
-
if (outcome === "stop") return "abandoned";
|
|
4162
|
+
if (outcome === "stop") return { status: "abandoned", endReason: "abandoned" };
|
|
4065
4163
|
}
|
|
4066
|
-
return "partial";
|
|
4164
|
+
return { status: "partial", endReason: "cap_actions" };
|
|
4067
4165
|
}
|
|
4068
4166
|
async decide(capture, momentId) {
|
|
4069
4167
|
const result = await this.deps.gateway.generate({
|
|
@@ -5022,6 +5120,11 @@ var RunEventLog = class {
|
|
|
5022
5120
|
|
|
5023
5121
|
// ../engine/src/orchestrator/coordinator.ts
|
|
5024
5122
|
init_src();
|
|
5123
|
+
var DEFAULT_SESSION_LIMITS = {
|
|
5124
|
+
maxActions: 40,
|
|
5125
|
+
maxElapsedMs: 15 * 6e4,
|
|
5126
|
+
maxTokens: 4e5
|
|
5127
|
+
};
|
|
5025
5128
|
var RunCoordinator = class {
|
|
5026
5129
|
constructor(input, deps) {
|
|
5027
5130
|
this.input = input;
|
|
@@ -5160,6 +5263,7 @@ var RunCoordinator = class {
|
|
|
5160
5263
|
*/
|
|
5161
5264
|
async runSessions(budgets) {
|
|
5162
5265
|
const { snapshot } = this.input;
|
|
5266
|
+
const limits = snapshot.sessionLimits ?? DEFAULT_SESSION_LIMITS;
|
|
5163
5267
|
const limit = Math.max(1, this.deps.maxConcurrentSessions ?? snapshot.cast.length);
|
|
5164
5268
|
const results = new Array(snapshot.cast.length);
|
|
5165
5269
|
let cursor = 0;
|
|
@@ -5181,7 +5285,7 @@ var RunCoordinator = class {
|
|
|
5181
5285
|
persona,
|
|
5182
5286
|
snapshot,
|
|
5183
5287
|
patienceBudget: budgets[index],
|
|
5184
|
-
limits
|
|
5288
|
+
limits
|
|
5185
5289
|
},
|
|
5186
5290
|
{
|
|
5187
5291
|
gateway: this.deps.gateway,
|
|
@@ -5223,12 +5327,17 @@ var RunCoordinator = class {
|
|
|
5223
5327
|
selfReportedReason: result.selfReportedReason,
|
|
5224
5328
|
patienceBudget: budgets[index],
|
|
5225
5329
|
patienceState: result.patienceState,
|
|
5226
|
-
limits
|
|
5330
|
+
limits,
|
|
5227
5331
|
startedAt: result.startedAt,
|
|
5228
5332
|
endedAt: result.endedAt,
|
|
5229
5333
|
error: result.error ? { kind: "internal", message: result.error.message, retryable: result.error.retryable, occurredAt: result.endedAt } : null,
|
|
5230
5334
|
contamination: null,
|
|
5231
5335
|
momentCount: result.moments.length,
|
|
5336
|
+
// Carried straight from the loop rather than derived from `status`, which cannot say: a
|
|
5337
|
+
// clean finish and an exhausted action budget are both `partial`. MONETIZATION "Refunds"
|
|
5338
|
+
// bills the first and refunds the second whole, so the distinction has to survive the trip
|
|
5339
|
+
// into the persisted record.
|
|
5340
|
+
endReason: result.endReason,
|
|
5232
5341
|
workerId: worker.id
|
|
5233
5342
|
};
|
|
5234
5343
|
this.sessionsByPersonaIndex.set(index, session);
|
|
@@ -6246,6 +6355,9 @@ function failureFrom(outcome) {
|
|
|
6246
6355
|
}
|
|
6247
6356
|
function describeOutcome(result) {
|
|
6248
6357
|
if (result.status === "error") return "the browser stopped unexpectedly";
|
|
6358
|
+
if (result.endReason === "cap_actions") return "ran out of actions before finishing";
|
|
6359
|
+
if (result.endReason === "cap_elapsed") return "ran out of time before finishing";
|
|
6360
|
+
if (result.endReason === "cap_tokens") return "ran out of its token budget before finishing";
|
|
6249
6361
|
if (result.selfReportedOutcome === "success") return "reached the goal";
|
|
6250
6362
|
if (result.selfReportedOutcome === "abandoned") return result.selfReportedReason ?? "gave up";
|
|
6251
6363
|
return result.selfReportedOutcome ?? "finished";
|
|
@@ -6639,6 +6751,10 @@ var StudyDesignProposalResponse = z23.object({
|
|
|
6639
6751
|
// ../app/src/services/billing.ts
|
|
6640
6752
|
init_src();
|
|
6641
6753
|
|
|
6754
|
+
// ../app/src/services/billing-outbox.ts
|
|
6755
|
+
init_src();
|
|
6756
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
6757
|
+
|
|
6642
6758
|
// ../app/src/application.ts
|
|
6643
6759
|
var PreflightFailedError = class extends Error {
|
|
6644
6760
|
constructor(preflight2) {
|
|
@@ -6651,6 +6767,9 @@ var PreflightFailedError = class extends Error {
|
|
|
6651
6767
|
preflight;
|
|
6652
6768
|
};
|
|
6653
6769
|
|
|
6770
|
+
// ../app/src/services/billing-webhooks.ts
|
|
6771
|
+
init_src();
|
|
6772
|
+
|
|
6654
6773
|
// ../app/src/services/export.ts
|
|
6655
6774
|
var DEFAULT_MAX_EMBEDDED_BYTES = 24 * 1024 * 1024;
|
|
6656
6775
|
|
|
@@ -6875,6 +6994,8 @@ var MCP_TOOLS = [
|
|
|
6875
6994
|
"propose_personas",
|
|
6876
6995
|
"approve_personas",
|
|
6877
6996
|
"run_preflight",
|
|
6997
|
+
"quote_study",
|
|
6998
|
+
"approve_quote",
|
|
6878
6999
|
"start_study",
|
|
6879
7000
|
"get_run_status",
|
|
6880
7001
|
"cancel_run",
|
|
@@ -6890,6 +7011,12 @@ var MUTATING_TOOLS = {
|
|
|
6890
7011
|
propose_personas: { mutates: false, capability: "study.write", idempotent: true },
|
|
6891
7012
|
approve_personas: { mutates: true, capability: "study.write", idempotent: true },
|
|
6892
7013
|
run_preflight: { mutates: false, capability: "study.write", idempotent: true },
|
|
7014
|
+
// BILLING_PLAN 3. Quoting is a builder operation and changes no money, so it is authorised the
|
|
7015
|
+
// same way `run_preflight` is. Approving the ceiling is what commits a run to spend, so it is
|
|
7016
|
+
// authorised as `run.start`: an operator can start runs and cannot manage billing, and requiring
|
|
7017
|
+
// `billing.manage` here would mean no operator could ever start a study.
|
|
7018
|
+
quote_study: { mutates: true, capability: "study.write", idempotent: false },
|
|
7019
|
+
approve_quote: { mutates: true, capability: "run.start", idempotent: true },
|
|
6893
7020
|
start_study: { mutates: true, capability: "run.start", idempotent: false },
|
|
6894
7021
|
get_run_status: { mutates: false, capability: "report.read", idempotent: true },
|
|
6895
7022
|
cancel_run: { mutates: true, capability: "run.cancel", idempotent: true },
|
|
@@ -6937,8 +7064,25 @@ var StartStudyInput = z25.object({
|
|
|
6937
7064
|
productId: z25.string(),
|
|
6938
7065
|
/** SPEC FR-116: supplied by the client so a retry finds the run rather than making a second. */
|
|
6939
7066
|
idempotencyKey: z25.string().min(1).max(120),
|
|
6940
|
-
maxCostCents: z25.number().positive().nullable().default(null)
|
|
7067
|
+
maxCostCents: z25.number().positive().nullable().default(null),
|
|
7068
|
+
/**
|
|
7069
|
+
* BILLING_PLAN 3: the run `quote_study` priced and `approve_quote` authorised.
|
|
7070
|
+
*
|
|
7071
|
+
* Optional in the schema and required in effect: a start with no run id gets a fresh one, which
|
|
7072
|
+
* by construction carries no approved quote, and `@aloud/app` refuses it. Typed as optional so
|
|
7073
|
+
* the refusal comes from the one authorization path rather than from an argument parser here,
|
|
7074
|
+
* which is SPEC FR-110: two enforcement points is one too many.
|
|
7075
|
+
*/
|
|
7076
|
+
runId: z25.string().optional()
|
|
6941
7077
|
});
|
|
7078
|
+
var QuoteStudyInput = z25.object({
|
|
7079
|
+
studyId: z25.string(),
|
|
7080
|
+
/** Re-quotes an existing prospective run instead of pricing a new one. */
|
|
7081
|
+
runId: z25.string().optional(),
|
|
7082
|
+
/** Overrides the archetype prediction of moments per participant. */
|
|
7083
|
+
momentsEach: z25.number().int().min(1).max(400).optional()
|
|
7084
|
+
});
|
|
7085
|
+
var ApproveQuoteInput = z25.object({ runId: z25.string() });
|
|
6942
7086
|
var RunIdInput = z25.object({ runId: z25.string() });
|
|
6943
7087
|
var FindingInput = z25.object({ runId: z25.string(), findingId: z25.string() });
|
|
6944
7088
|
var ShareLinkInput = z25.object({
|
|
@@ -7025,7 +7169,7 @@ async function handleTool(context, name, input, execute) {
|
|
|
7025
7169
|
castComposition: draft.cast.compositionFloor,
|
|
7026
7170
|
preflight: summarisePreflight(draft.preflight),
|
|
7027
7171
|
estimateCents: app.estimateFor(draft.study),
|
|
7028
|
-
note: "Nothing has run yet. Approve the cast, then start the
|
|
7172
|
+
note: "Nothing has run yet. Approve the cast, quote the study, get the ceiling approved, then start the run the quote named."
|
|
7029
7173
|
});
|
|
7030
7174
|
}
|
|
7031
7175
|
case "get_product_profile": {
|
|
@@ -7077,6 +7221,50 @@ async function handleTool(context, name, input, execute) {
|
|
|
7077
7221
|
const preflight2 = await app.preflight(actor, { workspaceId, studyId: parsed.studyId });
|
|
7078
7222
|
return json(summarisePreflight(preflight2));
|
|
7079
7223
|
}
|
|
7224
|
+
/**
|
|
7225
|
+
* BILLING_PLAN 3: "The MCP path enforces the same gate as the web path, through the one
|
|
7226
|
+
* authorization path in `@aloud/app`."
|
|
7227
|
+
*
|
|
7228
|
+
* That sentence is why this is three calls rather than one convenient one. An agent that could
|
|
7229
|
+
* start a study without a recorded agreement would be a way around the gate, and the whole
|
|
7230
|
+
* point of the gate is that a variable meter needs an agreement somebody actually made. So the
|
|
7231
|
+
* agent quotes, shows the human the range, gets the ceiling approved, and starts the run the
|
|
7232
|
+
* quote named.
|
|
7233
|
+
*/
|
|
7234
|
+
case "quote_study": {
|
|
7235
|
+
const parsed = QuoteStudyInput.parse(input);
|
|
7236
|
+
const quoted = await app.quoteStudy(actor, { workspaceId, ...parsed });
|
|
7237
|
+
return json({
|
|
7238
|
+
quoteId: quoted.quote.id,
|
|
7239
|
+
// Carried into approve_quote and start_study. The agreement names the run, so the run has
|
|
7240
|
+
// to be named before it exists.
|
|
7241
|
+
runId: quoted.quote.runId,
|
|
7242
|
+
resource: RESOURCE_SCHEMES.run(quoted.quote.runId),
|
|
7243
|
+
rateCardVersion: quoted.quote.rateCardVersion,
|
|
7244
|
+
estimateLowCredits: quoted.quote.estimateLowCredits,
|
|
7245
|
+
estimateHighCredits: quoted.quote.estimateHighCredits,
|
|
7246
|
+
approvedCapCredits: quoted.quote.approvedCapCredits,
|
|
7247
|
+
// MONETIZATION.md, "Quoting a study": the range is what a customer is promised and the
|
|
7248
|
+
// basis is how much that promise is worth, so the two travel together.
|
|
7249
|
+
basis: quoted.basis,
|
|
7250
|
+
observations: quoted.observations,
|
|
7251
|
+
approved: false,
|
|
7252
|
+
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."
|
|
7253
|
+
});
|
|
7254
|
+
}
|
|
7255
|
+
case "approve_quote": {
|
|
7256
|
+
const parsed = ApproveQuoteInput.parse(input);
|
|
7257
|
+
const quote = await app.approveQuote(actor, { workspaceId, runId: parsed.runId });
|
|
7258
|
+
return json({
|
|
7259
|
+
quoteId: quote.id,
|
|
7260
|
+
runId: quote.runId,
|
|
7261
|
+
approvedCapCredits: quote.approvedCapCredits,
|
|
7262
|
+
approvedBy: quote.approvedBy,
|
|
7263
|
+
approvedAt: quote.approvedAt,
|
|
7264
|
+
rateCardVersion: quote.rateCardVersion,
|
|
7265
|
+
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."
|
|
7266
|
+
});
|
|
7267
|
+
}
|
|
7080
7268
|
case "start_study": {
|
|
7081
7269
|
const parsed = StartStudyInput.parse(input);
|
|
7082
7270
|
let outcome;
|
|
@@ -7085,7 +7273,10 @@ async function handleTool(context, name, input, execute) {
|
|
|
7085
7273
|
workspaceId,
|
|
7086
7274
|
studyId: parsed.studyId,
|
|
7087
7275
|
idempotencyKey: parsed.idempotencyKey,
|
|
7088
|
-
maxCostCents: parsed.maxCostCents
|
|
7276
|
+
maxCostCents: parsed.maxCostCents,
|
|
7277
|
+
// BILLING_PLAN 3: the run the approved quote priced. Omitted, the gate in `@aloud/app`
|
|
7278
|
+
// refuses, which is the same refusal the web path gets.
|
|
7279
|
+
...parsed.runId ? { runId: parsed.runId } : {}
|
|
7089
7280
|
});
|
|
7090
7281
|
} catch (error) {
|
|
7091
7282
|
if (error instanceof PreflightFailedError) {
|
|
@@ -7238,7 +7429,9 @@ var TOOL_DESCRIPTIONS = {
|
|
|
7238
7429
|
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
7430
|
approve_personas: "Approve the cast a study will run with. Required before a study can start.",
|
|
7240
7431
|
run_preflight: "Check a study is safe and ready to run. Names the exact failing check rather than a generic failure.",
|
|
7241
|
-
|
|
7432
|
+
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.",
|
|
7433
|
+
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.",
|
|
7434
|
+
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
7435
|
get_run_status: "Read a run's current status, its participants, and its report if one exists yet.",
|
|
7243
7436
|
cancel_run: "Stop a run. Browsers are closed and no further model spend happens.",
|
|
7244
7437
|
list_findings: "List the findings from a completed run, with the participant count behind each one and the study's stated limitations.",
|
|
@@ -7299,11 +7492,18 @@ var TOOL_SHAPES = {
|
|
|
7299
7492
|
},
|
|
7300
7493
|
approve_personas: { studyId: z26.string(), cast: z26.array(z26.record(z26.string(), z26.unknown())) },
|
|
7301
7494
|
run_preflight: { studyId: z26.string() },
|
|
7495
|
+
quote_study: {
|
|
7496
|
+
studyId: z26.string(),
|
|
7497
|
+
runId: z26.string().describe("Re-quote a prospective run instead of pricing a new one.").optional(),
|
|
7498
|
+
momentsEach: z26.number().int().min(1).max(400).describe("Override the predicted moments per participant.").optional()
|
|
7499
|
+
},
|
|
7500
|
+
approve_quote: { runId: z26.string().describe("The runId quote_study returned.") },
|
|
7302
7501
|
start_study: {
|
|
7303
7502
|
studyId: z26.string(),
|
|
7304
7503
|
productId: z26.string(),
|
|
7305
7504
|
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()
|
|
7505
|
+
maxCostCents: z26.number().positive().nullable().optional(),
|
|
7506
|
+
runId: z26.string().describe("The runId from the approved quote. A run cannot start without one.").optional()
|
|
7307
7507
|
},
|
|
7308
7508
|
get_run_status: { runId: z26.string() },
|
|
7309
7509
|
cancel_run: { runId: z26.string(), reason: z26.string().optional() },
|
|
@@ -7328,6 +7528,11 @@ function createMcpServer(options) {
|
|
|
7328
7528
|
"not reports from real customers, and every response says how many participants hit a thing",
|
|
7329
7529
|
"rather than what share of users would.",
|
|
7330
7530
|
"",
|
|
7531
|
+
"A study costs money to run, so it cannot start until somebody has agreed to a ceiling.",
|
|
7532
|
+
"Call quote_study, show a person the range and the cap it returns, call approve_quote, then",
|
|
7533
|
+
"start_study with the same runId. The estimate is a range because how far a participant gets",
|
|
7534
|
+
"is the thing being measured, so there is no single price to quote.",
|
|
7535
|
+
"",
|
|
7331
7536
|
"start_study returns immediately. Poll get_run_status; nothing is lost if this connection drops.",
|
|
7332
7537
|
"",
|
|
7333
7538
|
"When you are running beside a product repository, call upsert_product_context with derived",
|
|
@@ -7737,32 +7942,46 @@ async function setup() {
|
|
|
7737
7942
|
const installed = onPath("aloud");
|
|
7738
7943
|
const latest = await latestVersion();
|
|
7739
7944
|
const stale = latest !== null && latest !== RUNNER_VERSION;
|
|
7945
|
+
const signedIn = await signedInState(credentials);
|
|
7740
7946
|
const out = (line = "") => process.stdout.write(line + "\n");
|
|
7741
7947
|
out();
|
|
7742
7948
|
out("Aloud runner setup. You are looking at the state of this machine.");
|
|
7743
7949
|
out();
|
|
7744
7950
|
out(` installed ${installed ? `yes (${RUNNER_VERSION})` : "no"}`);
|
|
7745
7951
|
out(` up to date ${latest === null ? "unknown, could not reach the registry" : stale ? `no, ${latest} is out` : "yes"}`);
|
|
7746
|
-
out(
|
|
7952
|
+
out(
|
|
7953
|
+
` 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"}`
|
|
7954
|
+
);
|
|
7747
7955
|
out(` chromium ${checks.chromiumInstalled ? "ready" : "downloads on first start, about 350 MB"}`);
|
|
7748
7956
|
out(` running ${running ? `yes (pid ${running.pid})` : "no"}`);
|
|
7749
7957
|
out();
|
|
7750
7958
|
const steps = [];
|
|
7751
7959
|
if (!installed) {
|
|
7752
|
-
steps.push("npm install -g @aloud/runner");
|
|
7960
|
+
steps.push(["npm install -g @aloud/runner"]);
|
|
7753
7961
|
} else if (stale) {
|
|
7754
|
-
steps.push(`npm install -g @aloud/runner@latest
|
|
7962
|
+
steps.push([`npm install -g @aloud/runner@latest`, `${RUNNER_VERSION} is installed, ${latest} is out.`]);
|
|
7755
7963
|
}
|
|
7756
|
-
if (
|
|
7757
|
-
steps.push(
|
|
7758
|
-
|
|
7759
|
-
|
|
7760
|
-
|
|
7761
|
-
|
|
7762
|
-
|
|
7964
|
+
if (signedIn.state === "revoked") {
|
|
7965
|
+
steps.push([
|
|
7966
|
+
`Create a new token at ${credentials?.server ?? DEFAULT_SERVER}/app/settings/runners`,
|
|
7967
|
+
"The saved one was revoked and cannot be reused."
|
|
7968
|
+
]);
|
|
7969
|
+
}
|
|
7970
|
+
if (signedIn.state === "none" || signedIn.state === "revoked") {
|
|
7971
|
+
steps.push([
|
|
7972
|
+
"aloud login",
|
|
7973
|
+
"Needs a terminal. If you are an agent, hand this step to the person: they run it",
|
|
7974
|
+
"themselves and paste the token at the prompt. Do not ask them to paste it to you."
|
|
7975
|
+
]);
|
|
7763
7976
|
}
|
|
7764
7977
|
if (!running) {
|
|
7765
|
-
steps.push("aloud start
|
|
7978
|
+
steps.push(["aloud start", "In a background shell. It never exits, so do not block on it."]);
|
|
7979
|
+
}
|
|
7980
|
+
if (steps.length === 0 && signedIn.state === "unreachable") {
|
|
7981
|
+
out(`Cannot reach ${signedIn.server}, so there is nothing useful to say about what is left.`);
|
|
7982
|
+
out("Check the connection and run this again.");
|
|
7983
|
+
out();
|
|
7984
|
+
return 1;
|
|
7766
7985
|
}
|
|
7767
7986
|
if (steps.length === 0) {
|
|
7768
7987
|
out("Nothing to do. This machine is set up and waiting for studies.");
|
|
@@ -7771,8 +7990,12 @@ async function setup() {
|
|
|
7771
7990
|
}
|
|
7772
7991
|
out("Do these, in order:");
|
|
7773
7992
|
out();
|
|
7774
|
-
for (const [index,
|
|
7775
|
-
|
|
7993
|
+
for (const [index, [command, ...notes]] of steps.entries()) {
|
|
7994
|
+
out(` ${index + 1}. ${command}`);
|
|
7995
|
+
for (const note of notes) out(` ${note}`);
|
|
7996
|
+
}
|
|
7997
|
+
out(` ${steps.length + 1}. aloud status`);
|
|
7998
|
+
out(" Exits non-zero until all of the above are true.");
|
|
7776
7999
|
out();
|
|
7777
8000
|
out("Rules, if you are an agent doing this:");
|
|
7778
8001
|
out();
|
|
@@ -7796,6 +8019,20 @@ async function setup() {
|
|
|
7796
8019
|
}
|
|
7797
8020
|
return 1;
|
|
7798
8021
|
}
|
|
8022
|
+
async function signedInState(credentials) {
|
|
8023
|
+
if (!credentials) return { state: "none" };
|
|
8024
|
+
try {
|
|
8025
|
+
const response = await fetch(new URL("api/runner/me", credentials.server + "/"), {
|
|
8026
|
+
headers: { authorization: `Bearer ${credentials.token}` },
|
|
8027
|
+
signal: AbortSignal.timeout(5e3)
|
|
8028
|
+
});
|
|
8029
|
+
if (response.status === 401 || response.status === 403) return { state: "revoked" };
|
|
8030
|
+
if (!response.ok) return { state: "unreachable", server: credentials.server };
|
|
8031
|
+
return { state: "ok", name: credentials.runnerName };
|
|
8032
|
+
} catch {
|
|
8033
|
+
return { state: "unreachable", server: credentials.server };
|
|
8034
|
+
}
|
|
8035
|
+
}
|
|
7799
8036
|
async function npmPrefix() {
|
|
7800
8037
|
const path = await new Promise((resolve) => {
|
|
7801
8038
|
const child = spawn2("npm", ["config", "get", "prefix"], { stdio: ["ignore", "pipe", "ignore"] });
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -210,6 +210,7 @@ async function setup(): Promise<number> {
|
|
|
210
210
|
const installed = onPath("aloud");
|
|
211
211
|
const latest = await latestVersion();
|
|
212
212
|
const stale = latest !== null && latest !== RUNNER_VERSION;
|
|
213
|
+
const signedIn = await signedInState(credentials);
|
|
213
214
|
|
|
214
215
|
const out = (line = "") => process.stdout.write(line + "\n");
|
|
215
216
|
|
|
@@ -218,27 +219,52 @@ async function setup(): Promise<number> {
|
|
|
218
219
|
out();
|
|
219
220
|
out(` installed ${installed ? `yes (${RUNNER_VERSION})` : "no"}`);
|
|
220
221
|
out(` up to date ${latest === null ? "unknown, could not reach the registry" : stale ? `no, ${latest} is out` : "yes"}`);
|
|
221
|
-
out(
|
|
222
|
+
out(
|
|
223
|
+
` signed in ${
|
|
224
|
+
signedIn.state === "ok"
|
|
225
|
+
? signedIn.name
|
|
226
|
+
: signedIn.state === "revoked"
|
|
227
|
+
? `no. The saved token for ${credentials?.runnerName ?? "this machine"} was revoked`
|
|
228
|
+
: signedIn.state === "unreachable"
|
|
229
|
+
? `cannot tell, ${signedIn.server} did not answer`
|
|
230
|
+
: "no"
|
|
231
|
+
}`,
|
|
232
|
+
);
|
|
222
233
|
out(` chromium ${checks.chromiumInstalled ? "ready" : "downloads on first start, about 350 MB"}`);
|
|
223
234
|
out(` running ${running ? `yes (pid ${running.pid})` : "no"}`);
|
|
224
235
|
out();
|
|
225
236
|
|
|
226
|
-
|
|
237
|
+
// Each step is a command plus the lines that qualify it. Only the command gets a number, or a
|
|
238
|
+
// continuation reads as a step of its own, and an agent following "step 2" ends up pasting a URL
|
|
239
|
+
// into a shell.
|
|
240
|
+
const steps: string[][] = [];
|
|
227
241
|
if (!installed) {
|
|
228
|
-
steps.push("npm install -g @aloud/runner");
|
|
242
|
+
steps.push(["npm install -g @aloud/runner"]);
|
|
229
243
|
} else if (stale) {
|
|
230
|
-
steps.push(`npm install -g @aloud/runner@latest
|
|
244
|
+
steps.push([`npm install -g @aloud/runner@latest`, `${RUNNER_VERSION} is installed, ${latest} is out.`]);
|
|
231
245
|
}
|
|
232
|
-
if (
|
|
233
|
-
steps.push(
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
246
|
+
if (signedIn.state === "revoked") {
|
|
247
|
+
steps.push([
|
|
248
|
+
`Create a new token at ${credentials?.server ?? DEFAULT_SERVER}/app/settings/runners`,
|
|
249
|
+
"The saved one was revoked and cannot be reused.",
|
|
250
|
+
]);
|
|
251
|
+
}
|
|
252
|
+
if (signedIn.state === "none" || signedIn.state === "revoked") {
|
|
253
|
+
steps.push([
|
|
254
|
+
"aloud login",
|
|
255
|
+
"Needs a terminal. If you are an agent, hand this step to the person: they run it",
|
|
256
|
+
"themselves and paste the token at the prompt. Do not ask them to paste it to you.",
|
|
257
|
+
]);
|
|
239
258
|
}
|
|
240
259
|
if (!running) {
|
|
241
|
-
steps.push("aloud start
|
|
260
|
+
steps.push(["aloud start", "In a background shell. It never exits, so do not block on it."]);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (steps.length === 0 && signedIn.state === "unreachable") {
|
|
264
|
+
out(`Cannot reach ${signedIn.server}, so there is nothing useful to say about what is left.`);
|
|
265
|
+
out("Check the connection and run this again.");
|
|
266
|
+
out();
|
|
267
|
+
return 1;
|
|
242
268
|
}
|
|
243
269
|
|
|
244
270
|
if (steps.length === 0) {
|
|
@@ -249,8 +275,12 @@ async function setup(): Promise<number> {
|
|
|
249
275
|
|
|
250
276
|
out("Do these, in order:");
|
|
251
277
|
out();
|
|
252
|
-
for (const [index,
|
|
253
|
-
|
|
278
|
+
for (const [index, [command, ...notes]] of steps.entries()) {
|
|
279
|
+
out(` ${index + 1}. ${command}`);
|
|
280
|
+
for (const note of notes) out(` ${note}`);
|
|
281
|
+
}
|
|
282
|
+
out(` ${steps.length + 1}. aloud status`);
|
|
283
|
+
out(" Exits non-zero until all of the above are true.");
|
|
254
284
|
out();
|
|
255
285
|
out("Rules, if you are an agent doing this:");
|
|
256
286
|
out();
|
|
@@ -277,6 +307,32 @@ async function setup(): Promise<number> {
|
|
|
277
307
|
return 1;
|
|
278
308
|
}
|
|
279
309
|
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Whether the saved credential still works, asked of the server rather than assumed from the file.
|
|
313
|
+
*
|
|
314
|
+
* A credentials file proves a token was written here once, not that it is still good. Revoking a
|
|
315
|
+
* machine from the web app leaves the file exactly as it was, so reporting "signed in" from its
|
|
316
|
+
* presence sent an agent off to `aloud start`, which died on its first poll with a 401 and no
|
|
317
|
+
* explanation of what to do. This is the same endpoint `login` checks a token against.
|
|
318
|
+
*/
|
|
319
|
+
async function signedInState(
|
|
320
|
+
credentials: Credentials | null,
|
|
321
|
+
): Promise<{ state: "none" } | { state: "ok"; name: string } | { state: "revoked" } | { state: "unreachable"; server: string }> {
|
|
322
|
+
if (!credentials) return { state: "none" };
|
|
323
|
+
try {
|
|
324
|
+
const response = await fetch(new URL("api/runner/me", credentials.server + "/"), {
|
|
325
|
+
headers: { authorization: `Bearer ${credentials.token}` },
|
|
326
|
+
signal: AbortSignal.timeout(5_000),
|
|
327
|
+
});
|
|
328
|
+
if (response.status === 401 || response.status === 403) return { state: "revoked" };
|
|
329
|
+
if (!response.ok) return { state: "unreachable", server: credentials.server };
|
|
330
|
+
return { state: "ok", name: credentials.runnerName };
|
|
331
|
+
} catch {
|
|
332
|
+
return { state: "unreachable", server: credentials.server };
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
280
336
|
/**
|
|
281
337
|
* Where npm would put a global install, and whether it can be written to.
|
|
282
338
|
*
|
package/src/run/execute.ts
CHANGED
|
@@ -336,8 +336,24 @@ function failureFrom(outcome: RunOutcome): string | null {
|
|
|
336
336
|
return null;
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
-
|
|
339
|
+
/**
|
|
340
|
+
* The one line the person watching their own machine sees when a participant stops.
|
|
341
|
+
*
|
|
342
|
+
* The cap cases are named rather than folded into "finished", which is what they used to read as.
|
|
343
|
+
* A session the runtime cut off produced truncated evidence, and MONETIZATION treats that as a
|
|
344
|
+
* platform failure rather than a delivery, so the operator watching it happen should be told that
|
|
345
|
+
* plainly instead of being shown a word that means the opposite.
|
|
346
|
+
*/
|
|
347
|
+
function describeOutcome(result: {
|
|
348
|
+
status: string;
|
|
349
|
+
endReason?: string | null;
|
|
350
|
+
selfReportedOutcome?: string | null;
|
|
351
|
+
selfReportedReason?: string | null;
|
|
352
|
+
}): string {
|
|
340
353
|
if (result.status === "error") return "the browser stopped unexpectedly";
|
|
354
|
+
if (result.endReason === "cap_actions") return "ran out of actions before finishing";
|
|
355
|
+
if (result.endReason === "cap_elapsed") return "ran out of time before finishing";
|
|
356
|
+
if (result.endReason === "cap_tokens") return "ran out of its token budget before finishing";
|
|
341
357
|
if (result.selfReportedOutcome === "success") return "reached the goal";
|
|
342
358
|
if (result.selfReportedOutcome === "abandoned") return result.selfReportedReason ?? "gave up";
|
|
343
359
|
return result.selfReportedOutcome ?? "finished";
|
package/src/version.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* package.json beside it to read, and importing one into the source trips the composite build's
|
|
11
11
|
* rootDir. `version.test.ts` asserts this matches, so the drift this invites cannot survive CI.
|
|
12
12
|
*/
|
|
13
|
-
export const RUNNER_VERSION = "0.2.
|
|
13
|
+
export const RUNNER_VERSION = "0.2.4";
|
|
14
14
|
|
|
15
15
|
/** The header the server reads it from. */
|
|
16
16
|
export const RUNNER_VERSION_HEADER = "x-aloud-runner-version";
|