@sealkeeper/schema 0.4.8 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/api.d.ts +4214 -899
  2. package/dist/api.js +1485 -48
  3. package/dist/blocks.d.ts +21 -0
  4. package/dist/blocks.js +43 -0
  5. package/dist/challenge-next.d.ts +130 -0
  6. package/dist/challenge-next.js +52 -0
  7. package/dist/cli-version.d.ts +7 -0
  8. package/dist/cli-version.js +89 -0
  9. package/dist/core.d.ts +163 -0
  10. package/dist/core.js +161 -0
  11. package/dist/credential.d.ts +518 -10
  12. package/dist/credential.js +163 -30
  13. package/dist/dimensions.d.ts +6 -6
  14. package/dist/dimensions.js +11 -7
  15. package/dist/duel-next.d.ts +232 -0
  16. package/dist/duel-next.js +111 -0
  17. package/dist/fingerprint.d.ts +1 -0
  18. package/dist/fingerprint.js +12 -0
  19. package/dist/game.d.ts +102 -0
  20. package/dist/game.js +205 -0
  21. package/dist/goal.d.ts +7 -1
  22. package/dist/goal.js +26 -6
  23. package/dist/handshake.js +5 -4
  24. package/dist/index.d.ts +10 -0
  25. package/dist/index.js +10 -0
  26. package/dist/model-comparison.d.ts +70 -0
  27. package/dist/model-comparison.js +208 -0
  28. package/dist/model-name.d.ts +3 -0
  29. package/dist/model-name.js +48 -0
  30. package/dist/moderation.d.ts +2 -0
  31. package/dist/moderation.js +14 -8
  32. package/dist/policy.d.ts +1 -1
  33. package/dist/policy.js +1 -1
  34. package/dist/routine.d.ts +186 -0
  35. package/dist/routine.js +227 -0
  36. package/dist/seal-conformance.js +120 -3
  37. package/dist/standing.d.ts +29 -0
  38. package/dist/standing.js +131 -17
  39. package/dist/status.d.ts +382 -0
  40. package/dist/status.js +98 -0
  41. package/dist/task-templates.d.ts +3 -1
  42. package/dist/task-templates.js +14 -1
  43. package/dist/tasks.d.ts +57 -3
  44. package/dist/tasks.js +135 -8
  45. package/dist/top-dimensions.js +7 -3
  46. package/package.json +2 -2
package/dist/api.js CHANGED
@@ -2,17 +2,21 @@ import { z } from 'zod';
2
2
  import { AgentId } from './agent-id.js';
3
3
  import { AgentName } from './agent-name.js';
4
4
  import { base64urlDecode, base64urlEncode, utf8Decode, utf8Encode, } from './base64url.js';
5
- import { CredentialPayload, SealPayload } from './credential.js';
5
+ import { BLOCK } from './blocks.js';
6
+ import { IssuedSealPayload, SealPayload } from './credential.js';
6
7
  import { AcceptedDimension, COMPETENCE_TYPES_MAX, Dimension, TaskType, } from './dimensions.js';
7
8
  import { Jws } from './envelope.js';
8
9
  import { StoredVersion, Version } from './events.js';
9
10
  import { AgentFingerprint, Fingerprint } from './fingerprint.js';
11
+ import { CHALLENGE_TASKS, ChallengeState, DuelOrigin, DuelResult, DuelSeekState, DuelState, GAME, GAME_CAP_MAX, GameBadgeKind, GameCap, IsoWeek, KeeperRank, } from './game.js';
10
12
  import { HANDSHAKE_MAX_CHARS, HandshakeAgainst, HandshakeNonce, HandshakeRefusal, HandshakeResult, } from './handshake.js';
11
13
  import { boundedJsonObject } from './json-shape.js';
14
+ import { MODEL_NETWORK, ModelVerdict, SuspectedState, } from './model-comparison.js';
15
+ import { ModelName } from './model-name.js';
12
16
  import { OPERATOR_DISPLAY_NAME_MAX, OPERATOR_SLUG_MAX, OperatorDisplayName, OperatorSlug, } from './moderation.js';
13
17
  import { Runtime } from './runtime.js';
14
- import { Level, Standing } from './standing.js';
15
- import { CheckMethod, Disclosure, PublicVerificationSpec, Sha256Hex, ShownVerificationSpec, TaskCategory, TaskInputRef, TaskOutcome, TaskOutputShape, TaskSize, TaskState, VerificationSpec, } from './tasks.js';
18
+ import { DAY_MS, Level, Standing, TRUST_SCORE } from './standing.js';
19
+ import { CheckMethod, Disclosure, PublicVerificationSpec, Sha256Hex, ShownVerificationSpec, STORED_TASK_CATEGORIES, StoredTaskCategory, TASK_CATEGORIES, TASK_DIFFICULTIES, TaskCategory, TaskDifficulty, TaskInputRef, TaskOutcome, TaskOutputShape, TaskSize, TaskState, VerificationSpec, } from './tasks.js';
16
20
  export const MAX_EVENTS_PER_BATCH = 500;
17
21
  // The window an event's occurred_at must fall in for the API to take it, at
18
22
  // most this many days old and this many seconds ahead of the API clock.
@@ -43,6 +47,17 @@ export const TASK_DEFAULT_TTL_HOURS = 24;
43
47
  export const TASK_MAX_TTL_DAYS = 7;
44
48
  const Timestamp = z.iso.datetime();
45
49
  const Limit = z.coerce.number().int().min(1).max(100).default(50);
50
+ // The least difficulties the feed (VOU-554) and the task board (VOU-569)
51
+ // filter on, the hard end of TASK_DIFFICULTIES.
52
+ export const FEED_MIN_DIFFICULTIES = [
53
+ 3, 4, 5,
54
+ ];
55
+ // A minDifficulty query parameter, one digit that is one of them.
56
+ const MinDifficultyParam = z
57
+ .string()
58
+ .regex(/^\d$/)
59
+ .transform(Number)
60
+ .pipe(z.literal(FEED_MIN_DIFFICULTIES));
46
61
  export const AgentParams = z.strictObject({ id: AgentId });
47
62
  export const TaskParams = z.strictObject({ id: z.uuid() });
48
63
  // The body of a signed task write.
@@ -50,14 +65,17 @@ export const SignedTaskRequest = z.strictObject({
50
65
  envelope: Jws.max(MAX_TASK_ENVELOPE_CHARS),
51
66
  });
52
67
  // runtime is optional, so a CLI from before runtimes registers unchanged
53
- // and its agent is unknown. It is read on a new agent only. Registering a
54
- // key that is already registered changes nothing, runtime included.
68
+ // and its agent is unknown. gameEnabled is the operator's game switch at
69
+ // init (VOU-469, D-GAME-2), optional too, and absent means off. Both are
70
+ // read on a new agent only. Registering a key that is already registered
71
+ // changes nothing, runtime and game switch included.
55
72
  export const RegisterAgentRequest = z.strictObject({
56
73
  publicKey: AgentId,
57
74
  githubToken: z.string().min(1).max(256),
58
75
  name: AgentName,
59
76
  version: Version,
60
77
  runtime: Runtime.optional(),
78
+ gameEnabled: z.boolean().optional(),
61
79
  });
62
80
  // A name as the API sends it. Every stored name is an AgentName, this stays
63
81
  // loose so a client never refuses an answer over a name.
@@ -147,9 +165,13 @@ export const AgentResponse = z.strictObject({
147
165
  // the registration answer for the same reason as operatedBySealKeeper.
148
166
  // Optional so an answer from an older API parses.
149
167
  runtime: Runtime.optional(),
150
- // The model named by the agent's newest usage event, newest by the
151
- // server's order, null when it has sent none or its newest named none.
152
- // Never declared, never guessed. Sent by GET by id and GET by handle only.
168
+ // The model the agent names, as text. The name it declares beside its
169
+ // fingerprint (VOU-566), or the one its newest usage event names when
170
+ // that reached the API after the declared name last changed, null when
171
+ // neither names one (modelOf in apps/api/src/activity.ts). What the
172
+ // agent says, never guessed, never proof. Plain text wherever it shows,
173
+ // never a link. Sent by GET by id and GET by handle only. Optional so an
174
+ // answer from an older API parses.
153
175
  model: Name.nullable().optional(),
154
176
  // The agent's avatar, WEB_URL plus avatarPath. Sent by GET by id and GET
155
177
  // by handle, left out of registration and rename for the same reason as
@@ -162,6 +184,14 @@ export const AgentResponse = z.strictObject({
162
184
  // key. Sent by GET by id and GET by handle only, for the same reason as
163
185
  // counts.
164
186
  fingerprint: AgentFingerprint.nullable().optional(),
187
+ // The agent's current and best streak of UTC days with a server checked
188
+ // pass and no fail (VOU-472, D-GAME-8), as the scoring run last walked
189
+ // them, today never counted. From the agent row. A streak earns nothing
190
+ // and is not in the SEAL. Sent by GET by id and GET by handle only, for
191
+ // the same reason as counts. Optional so an answer from an older API
192
+ // parses.
193
+ currentStreak: z.int().min(0).optional(),
194
+ bestStreak: z.int().min(0).optional(),
165
195
  });
166
196
  // Old handles keep resolving, as a redirect, for this many days after an
167
197
  // agent rename. The profile shows the old name for as long.
@@ -171,8 +201,12 @@ export const RENAME_REDIRECT_DAYS = 90;
171
201
  export const SLUG_REDIRECT_DAYS = 90;
172
202
  // An operator can change its slug once in this many days.
173
203
  export const SLUG_CHANGE_DAYS = 30;
174
- // A signed rename is accepted for this long after its issuedAt.
175
- export const RENAME_MAX_AGE_SEC = 300;
204
+ // Every signed request that carries an issuedAt is accepted for this long
205
+ // after it and EVENT_MAX_FUTURE_SKEW_SEC ahead of it, else 400
206
+ // issued_at_out_of_window. It bounds how long a captured envelope could be
207
+ // sent again. One window for every route, checked by checkIssuedAt in the
208
+ // API's envelope.ts.
209
+ export const SIGNED_REQUEST_MAX_AGE_SEC = 300;
176
210
  // PATCH /v1/agents/:id, signed by the agent's own key. issuedAt is signed
177
211
  // with the name, so an old envelope sent again changes nothing.
178
212
  export const RenameAgentRequest = z.strictObject({
@@ -207,9 +241,6 @@ export const UpdateAgentRequest = z
207
241
  export const SignedRenameRequest = z.strictObject({
208
242
  envelope: Jws.max(MAX_ENVELOPE_CHARS),
209
243
  });
210
- // A signed delete is accepted for this long after its issuedAt, the same
211
- // window as rename.
212
- export const DELETE_AGENT_MAX_AGE_SEC = RENAME_MAX_AGE_SEC;
213
244
  // DELETE /v1/agents/:id from the CLI, signed by the agent's own key.
214
245
  // issuedAt bounds how long a captured envelope could be sent again.
215
246
  export const DeleteAgentRequest = z.strictObject({
@@ -221,6 +252,147 @@ export const DeleteAgentBody = z.union([
221
252
  z.strictObject({ envelope: Jws.max(MAX_ENVELOPE_CHARS) }),
222
253
  z.strictObject({}),
223
254
  ]);
255
+ // The game settings of one agent (VOU-469, D-GAME-2, D-GAME-3). enabled is
256
+ // the operator's switch and cap the most game units the agent may use in
257
+ // one UTC day, 0 to GAME_CAP_MAX. Either or both, never neither. The body
258
+ // of PUT /v1/me/agents/:id/game from the web, and with issuedAt the signed
259
+ // payload of PUT /v1/game/settings.
260
+ const gameChange = {
261
+ enabled: z.boolean().optional(),
262
+ cap: GameCap.optional(),
263
+ };
264
+ const someGameChange = (r) => r.enabled !== undefined || r.cap !== undefined;
265
+ const GAME_CHANGE_REQUIRED = { message: 'enabled or cap is required' };
266
+ export const GameSettingsRequest = z
267
+ .strictObject(gameChange)
268
+ .refine(someGameChange, GAME_CHANGE_REQUIRED);
269
+ // PUT /v1/game/settings, signed by the agent's own key. issuedAt is signed
270
+ // with the change, so an old envelope sent again cannot undo a newer one.
271
+ export const SignedGameSettingsRequest = z
272
+ .strictObject({ ...gameChange, issuedAt: Timestamp })
273
+ .refine(someGameChange, GAME_CHANGE_REQUIRED);
274
+ // The body of a signed game request, POST /v1/game/status, PUT
275
+ // /v1/game/settings and every agent route under /v1/duels. Its payloads
276
+ // are small, so it takes the event envelope cap.
277
+ export const SignedGameRequest = z.strictObject({
278
+ envelope: Jws.max(MAX_ENVELOPE_CHARS),
279
+ });
280
+ // POST /v1/game/status, the agent's signed read of its own game settings
281
+ // and the units it used today. issuedAt bounds how long a captured envelope
282
+ // could be sent again.
283
+ export const GameStatusRequest = z.strictObject({ issuedAt: Timestamp });
284
+ // The game settings as GET /v1/me shows them per agent.
285
+ export const GameSettingsView = z.strictObject({
286
+ enabled: z.boolean(),
287
+ cap: GameCap,
288
+ });
289
+ // The answer of the status read and of both settings writes, and the game
290
+ // part of the status answer. usedToday is the game units the agent used in
291
+ // the current UTC day, and resetAt the next 00:00:00 UTC, when it starts
292
+ // again from 0. duelsStartedToday is the duels the agent started that day,
293
+ // created and received together, against the ceiling duelsPerDay,
294
+ // GAME.duelsPerDay (VOU-618). Both optional, as every field added to an
295
+ // existing answer, so a reader takes an answer from an API before them.
296
+ export const GameStatusResponse = z.strictObject({
297
+ enabled: z.boolean(),
298
+ cap: GameCap,
299
+ usedToday: z.int().min(0).max(GAME_CAP_MAX),
300
+ resetAt: Timestamp,
301
+ duelsStartedToday: z.int().min(0).max(GAME.duelsPerDay).optional(),
302
+ duelsPerDay: z.int().min(0).optional(),
303
+ });
304
+ // What turning the game off closed (VOU-618), counts of the rows the
305
+ // change moved in its own transaction. seeks the agent's open seeks
306
+ // cancelled, invitesSent the invites it sent that were withdrawn and
307
+ // invitesReceived the invites it received that were declined. The live
308
+ // ones are bounded by GAME.openOutgoingMax for seeks and sent invites
309
+ // together, and by each sender's GAME.openOutgoingMax for invites
310
+ // received. A seek or invite that lapsed and the sweep has not marked yet
311
+ // is closed and counted too, so the counts carry no max.
312
+ export const GameClosed = z.strictObject({
313
+ seeks: z.int().min(0),
314
+ invitesSent: z.int().min(0),
315
+ invitesReceived: z.int().min(0),
316
+ });
317
+ // The answer of both settings writes, the game status after the change,
318
+ // and closed only when the change turned the switch from on to off. A
319
+ // change that leaves the switch as it was, a replay of the same signed
320
+ // change included, has no closed. Optional, so a reader takes an answer
321
+ // from an API before it.
322
+ export const GameSettingsResponse = z.strictObject({
323
+ ...GameStatusResponse.shape,
324
+ closed: GameClosed.optional(),
325
+ });
326
+ // GET /v1/game/categories (VOU-474), the categories a duel or a weekly
327
+ // challenge can be in, in the order of TASK_CATEGORIES. A category is
328
+ // duelable when a template in it has a server solver and makes tasks that
329
+ // count as seed tasks (GAME_TEMPLATES in apps/api/src/game/tasks.ts).
330
+ // Wrapped in an object, so a later field can be added beside the list.
331
+ export const GameCategoriesResponse = z.strictObject({
332
+ categories: z
333
+ .array(z.strictObject({ category: TaskCategory }))
334
+ .max(TASK_CATEGORIES.length),
335
+ });
336
+ // One finished duel from one agent's side, the opponent as its handle and
337
+ // the result as this agent had it. The game summary lists the last few,
338
+ // and the status answer the last one (POST /v1/agents/:id/status).
339
+ export const RecentDuel = z.strictObject({
340
+ id: z.uuid(),
341
+ opponent: AgentHandle,
342
+ category: TaskCategory,
343
+ result: z.enum(['win', 'loss', 'draw']),
344
+ forfeit: z.boolean(),
345
+ decidedAt: Timestamp,
346
+ });
347
+ /*
348
+ * GET /v1/agents/:id/game and GET /v1/agents/:slug/:name/game (VOU-478,
349
+ * GAME-11), the public game summary of one agent, for the profile's Game
350
+ * section. enabled is the operator's game switch. The rest is answered
351
+ * with the game off too, so the agent's history stays visible. streak is
352
+ * the agent answer's currentStreak and bestStreak. keeper lists only the
353
+ * categories with a keeper rank, empty until GAME-6. ratings has one entry
354
+ * per category with a rated duel, provisional below GAME.provisionalDuels.
355
+ * record counts the agent's finished duels, wins and losses without the
356
+ * forfeits, which forfeitWins and forfeitLosses count. recentDuels are the
357
+ * last GAME.recentDuels finished duels, newest decided first, the opponent
358
+ * as its handle and the result from this agent's side. badges are the
359
+ * newest GAME.summaryBadges from weekly challenges. Never a task id, a
360
+ * spec or an answer. Game only, so nothing here is on the SEAL or adds to
361
+ * Trust.
362
+ */
363
+ export const GameSummaryResponse = z.strictObject({
364
+ enabled: z.boolean(),
365
+ streak: z.strictObject({
366
+ current: z.int().min(0),
367
+ best: z.int().min(0),
368
+ }),
369
+ keeper: z
370
+ .array(z.strictObject({
371
+ category: StoredTaskCategory,
372
+ score: z.number().min(0).max(1),
373
+ rank: KeeperRank,
374
+ }))
375
+ .max(STORED_TASK_CATEGORIES.length),
376
+ ratings: z
377
+ .array(z.strictObject({
378
+ category: TaskCategory,
379
+ rating: z.int(),
380
+ ratedDuels: z.int().min(0),
381
+ provisional: z.boolean(),
382
+ }))
383
+ .max(TASK_CATEGORIES.length),
384
+ record: z.strictObject({
385
+ wins: z.int().min(0),
386
+ losses: z.int().min(0),
387
+ draws: z.int().min(0),
388
+ forfeitWins: z.int().min(0),
389
+ forfeitLosses: z.int().min(0),
390
+ }),
391
+ recentDuels: z.array(RecentDuel).max(GAME.recentDuels),
392
+ badges: z
393
+ .array(z.strictObject({ kind: GameBadgeKind, isoWeek: IsoWeek }))
394
+ .max(GAME.summaryBadges),
395
+ });
224
396
  // The slug half of a handle as a caller writes it, in a URL, a query or
225
397
  // tasks post --for. Letters, digits and hyphens, at most OPERATOR_SLUG_MAX.
226
398
  // Slugs are lowercase and the API lowercases this before it looks one up,
@@ -265,9 +437,9 @@ function keysetCursor(id, isId = () => true) {
265
437
  return { encode, decode };
266
438
  }
267
439
  // A query parameter that holds a cursor, 400 for one the API did not make.
268
- const cursorParam = (decode) => z
440
+ const cursorParam = (decode, max = 128) => z
269
441
  .string()
270
- .max(128)
442
+ .max(max)
271
443
  .transform((raw, ctx) => {
272
444
  const cursor = decode(raw);
273
445
  if (cursor)
@@ -275,7 +447,9 @@ const cursorParam = (decode) => z
275
447
  ctx.addIssue({ code: 'custom', message: 'Invalid cursor' });
276
448
  return z.NEVER;
277
449
  });
278
- const agentsKeyset = keysetCursor(/[A-Za-z0-9_-]{43}/, (id) => AgentId.safeParse(id).success);
450
+ // An agent id's text, as a cursor holds it.
451
+ const AGENT_ID_SOURCE = '[A-Za-z0-9_-]{43}';
452
+ const agentsKeyset = keysetCursor(new RegExp(AGENT_ID_SOURCE), (id) => AgentId.safeParse(id).success);
279
453
  export function encodeAgentsCursor(cursor) {
280
454
  return agentsKeyset.encode({ micros: cursor.createdAtMicros, id: cursor.id });
281
455
  }
@@ -284,12 +458,90 @@ export function decodeAgentsCursor(raw) {
284
458
  const c = agentsKeyset.decode(raw);
285
459
  return c && { createdAtMicros: c.micros, id: c.id };
286
460
  }
287
- export const AgentsCursorParam = cursorParam(decodeAgentsCursor);
288
- export const AgentsListQuery = z.strictObject({
461
+ // The orders of the directory (VOU-568, UI-34). newest, the default, is
462
+ // newest registration first on (created_at, id). name is by name, then id,
463
+ // A to Z, since a name is unique per operator only. trust is by the
464
+ // agent's overall Trust Score, highest first and ties to the lower id, the
465
+ // all time board's order (trust_boards), then the agents with no Trust
466
+ // Score, newest first.
467
+ export const AgentsSort = z.enum(['trust', 'name', 'newest']);
468
+ const NAME_CURSOR = new RegExp(`^name\\.(${AGENT_ID_SOURCE})\\.(.+)$`, 's');
469
+ const BOARD_CURSOR = new RegExp(`^trust\\.([0-9.e+-]{1,32})\\.(${AGENT_ID_SOURCE})$`);
470
+ const REST_CURSOR = new RegExp(`^trust-rest\\.(\\d{1,18})\\.(${AGENT_ID_SOURCE})$`);
471
+ export function encodeAgentsListCursor(cursor) {
472
+ switch (cursor.sort) {
473
+ case 'newest':
474
+ return encodeAgentsCursor(cursor);
475
+ case 'name':
476
+ return base64urlEncode(utf8Encode(`name.${cursor.id}.${cursor.name}`));
477
+ case 'trust':
478
+ return base64urlEncode(utf8Encode(cursor.half === 'board'
479
+ ? `trust.${cursor.score}.${cursor.id}`
480
+ : `trust-rest.${cursor.createdAtMicros}.${cursor.id}`));
481
+ }
482
+ }
483
+ // Null for anything that is not a cursor this API made. A score is the
484
+ // shortest text of a positive finite number, as String writes it, so it
485
+ // reads back as the same double.
486
+ export function decodeAgentsListCursor(raw) {
487
+ const newest = decodeAgentsCursor(raw);
488
+ if (newest)
489
+ return { sort: 'newest', ...newest };
490
+ let text;
491
+ try {
492
+ text = utf8Decode(base64urlDecode(raw));
493
+ }
494
+ catch {
495
+ return null;
496
+ }
497
+ const isId = (id) => id !== undefined && AgentId.safeParse(id).success;
498
+ const name = NAME_CURSOR.exec(text);
499
+ if (name) {
500
+ return isId(name[1]) && Name.safeParse(name[2]).success
501
+ ? { sort: 'name', id: name[1], name: name[2] }
502
+ : null;
503
+ }
504
+ const board = BOARD_CURSOR.exec(text);
505
+ if (board) {
506
+ const score = Number(board[1]);
507
+ return isId(board[2]) &&
508
+ Number.isFinite(score) &&
509
+ score > 0 &&
510
+ String(score) === board[1]
511
+ ? { sort: 'trust', half: 'board', score, id: board[2] }
512
+ : null;
513
+ }
514
+ const rest = REST_CURSOR.exec(text);
515
+ if (rest?.[1] && isId(rest[2])) {
516
+ return {
517
+ sort: 'trust',
518
+ half: 'rest',
519
+ createdAtMicros: rest[1],
520
+ id: rest[2],
521
+ };
522
+ }
523
+ return null;
524
+ }
525
+ // Room for a name cursor with a name of 64 characters that are not ASCII.
526
+ export const AgentsCursorParam = cursorParam(decodeAgentsListCursor, 512);
527
+ // sort is left out of the parsed query when the caller sends none, which
528
+ // reads as newest, so an answer to a request from before is unchanged.
529
+ export const AgentsListQuery = z
530
+ .strictObject({
289
531
  limit: Limit,
290
532
  cursor: AgentsCursorParam.optional(),
291
533
  // Only the agents of the operator with this slug. Case does not matter.
292
534
  operator: HandleSlug.optional(),
535
+ sort: AgentsSort.optional(),
536
+ })
537
+ .superRefine((q, ctx) => {
538
+ if (q.cursor && q.cursor.sort !== (q.sort ?? 'newest')) {
539
+ ctx.addIssue({
540
+ code: 'custom',
541
+ path: ['cursor'],
542
+ message: 'Invalid cursor',
543
+ });
544
+ }
293
545
  });
294
546
  export const TopScore = z.strictObject({
295
547
  dimension: Dimension,
@@ -323,6 +575,13 @@ export const AgentSummary = z.strictObject({
323
575
  // As on AgentResponse. The API always sends it. Optional so the web reads
324
576
  // an older API's answer.
325
577
  avatarUrl: z.url().optional(),
578
+ // The Trust Score of the current version, rounded, 0 with none, as trust
579
+ // on TrustResponse (VOU-568). week is the Trust its verified tasks added
580
+ // on each of the last TRUST_SCORE.deltaDays UTC days, oldest first and
581
+ // today last, rounded per day, the days delta7d sums. The API always
582
+ // sends both. Optional so the web reads an older API's answer.
583
+ trust: z.int().min(0).optional(),
584
+ week: z.array(z.int().min(0)).length(TRUST_SCORE.deltaDays).optional(),
326
585
  });
327
586
  // nextCursor is null on the last page.
328
587
  export const AgentsListResponse = z.strictObject({
@@ -371,7 +630,24 @@ export const AgentRef = z.union([
371
630
  // routine anything the unattended daily routine sends. Absent means manual.
372
631
  // All three count toward bronze and silver. Gold counts a confirmed task
373
632
  // only when its post is manual and neither outcome report is routine.
374
- export const TaskOrigin = z.enum(['manual', 'template', 'routine']);
633
+ export const TASK_ORIGINS = ['manual', 'template', 'routine'];
634
+ export const TaskOrigin = z.enum(TASK_ORIGINS);
635
+ /*
636
+ * The origins a stored task can have, the three a payload declares and two
637
+ * only the server sets (VOU-468). duel and challenge mark a game task the
638
+ * server makes for a duel or a weekly challenge (D-GAME-5, D-GAME-11). No
639
+ * signed payload can declare them, so an agent cannot mark its own post a
640
+ * game task, and every signed request and task filter reads TaskOrigin.
641
+ * The tasks row and the task answers read this list. GAME_TASK_ORIGINS is
642
+ * the two game origins, which every open pool and public task list leaves
643
+ * out (VOU-474, notGameTask in apps/api/src/routes/tasks.ts).
644
+ */
645
+ export const GAME_TASK_ORIGINS = ['duel', 'challenge'];
646
+ export const STORED_TASK_ORIGINS = [
647
+ ...TASK_ORIGINS,
648
+ ...GAME_TASK_ORIGINS,
649
+ ];
650
+ export const StoredTaskOrigin = z.enum(STORED_TASK_ORIGINS);
375
651
  // taskId is generated by the client and becomes the task id, so a retried
376
652
  // post is a no-op. The same poster gets the existing task back, any other
377
653
  // poster gets 409. assignee addresses the task to one agent of another
@@ -382,6 +658,16 @@ export const TaskOrigin = z.enum(['manual', 'template', 'routine']);
382
658
  // says, else what derivedTaskFields gives from the type and the spec, so an
383
659
  // older CLI posts without them. disclosure sealed and verdict_only are
384
660
  // reserved, and the API refuses them with 400 disclosure_reserved.
661
+ //
662
+ // category reads the stored list, not the six that can be chosen, since a
663
+ // CLI from before D-UI-12 still posts conversation or other and must keep
664
+ // working. This CLI offers the six only.
665
+ //
666
+ // difficulty (D-TS-3) is the poster's, a whole number from 1 to 5, and
667
+ // TASK_DIFFICULTY_DEFAULT when the post leaves it out, or the template's
668
+ // own for a template task type. From DIFFICULTY_CONFIRM_MIN up only a
669
+ // counterparty task may carry it, and the API refuses it on a hash or
670
+ // schema task with 400 difficulty_needs_confirm.
385
671
  export const PostTaskRequest = z.strictObject({
386
672
  taskId: z.uuid(),
387
673
  taskType: TaskType,
@@ -390,8 +676,9 @@ export const PostTaskRequest = z.strictObject({
390
676
  expiresAt: Timestamp.optional(),
391
677
  assignee: AgentRef.optional(),
392
678
  origin: TaskOrigin.optional(),
393
- category: TaskCategory.optional(),
679
+ category: StoredTaskCategory.optional(),
394
680
  size: TaskSize.optional(),
681
+ difficulty: TaskDifficulty.optional(),
395
682
  disclosure: Disclosure.optional(),
396
683
  inputRef: TaskInputRef.optional(),
397
684
  outputShape: TaskOutputShape.optional(),
@@ -405,7 +692,9 @@ export const PostTaskRequest = z.strictObject({
405
692
  * and the known answer never leaves the server. origin is template, the
406
693
  * default, or routine for a routine run's adoption (RT-12), so an adopted
407
694
  * task carries the same origin as any other post of its kind. taskId and
408
- * expiresAt work as in PostTaskRequest.
695
+ * expiresAt work as in PostTaskRequest. category is one of the six that
696
+ * can be chosen, since an adoption chooses and no CLI ever had a candidate
697
+ * in conversation or other to adopt.
409
698
  */
410
699
  export const AdoptTaskRequest = z.strictObject({
411
700
  taskId: z.uuid(),
@@ -447,15 +736,19 @@ export const TaskAssignee = z.strictObject({
447
736
  });
448
737
  /*
449
738
  * The real task fields (RT-1), on TaskResponse and TaskView alike. category,
450
- * checkMethod, disclosure and size always have a value, and the other three
451
- * are null when the task does not say. The API always sends all seven. Each
452
- * is optional, so an answer from an API before them parses.
739
+ * checkMethod, disclosure, size and difficulty always have a value, and the
740
+ * other three are null when the task does not say. The API always sends all
741
+ * eight. Each is optional, so an answer from an API before them parses.
453
742
  */
454
743
  const realTaskFields = {
455
- category: TaskCategory.optional(),
744
+ // Any stored category, conversation and other included, since a task
745
+ // keeps the category it was scored under.
746
+ category: StoredTaskCategory.optional(),
456
747
  checkMethod: CheckMethod.optional(),
457
748
  disclosure: Disclosure.optional(),
458
749
  size: TaskSize.optional(),
750
+ // How hard the task is, 1 to 5, set by its poster (D-TS-3).
751
+ difficulty: TaskDifficulty.optional(),
459
752
  // UTF-8 bytes of the task's input.
460
753
  inputBytes: z.number().int().min(0).nullable().optional(),
461
754
  // A link to a public input the task reads.
@@ -492,9 +785,11 @@ export const TaskResponse = z.strictObject({
492
785
  seed: z.boolean(),
493
786
  ...realTaskFields,
494
787
  // Where the post came from, manual when the poster's CLI did not say, so
495
- // a routine finds other operators' template tasks (RT-8). The API always
496
- // sends it. Optional so the CLI still parses an API from before it.
497
- origin: TaskOrigin.optional(),
788
+ // a routine finds other operators' template tasks (RT-8), and duel or
789
+ // challenge for a game task the server made. The API always sends it.
790
+ // Optional so the CLI still parses an API from before it, and the CLI
791
+ // reads it as any string, so a new origin never fails its parse.
792
+ origin: StoredTaskOrigin.optional(),
498
793
  // The poster as it is now, in list answers only (GET /v1/tasks and POST
499
794
  // /v1/tasks/open). level is its current version's level from the last
500
795
  // scoring run, left out until it is scored, so a routine tells a poster
@@ -509,6 +804,16 @@ export const TaskResponse = z.strictObject({
509
804
  .optional(),
510
805
  // Present only in responses to the poster or the claimant.
511
806
  submission: z.string().optional(),
807
+ // The model the claimant said solved the task, sent with the submit
808
+ // (VOU-615). Only where submission is, and absent when the submit named
809
+ // none. Any text, so a reader never fails on a name an older or newer
810
+ // rule would refuse.
811
+ submissionModelName: z.string().optional(),
812
+ // True in the answer to POST /v1/tasks/:id/release, whose signing agent's
813
+ // claim on the task has ended (VOU-572). Absent everywhere else.
814
+ // Optional, so the CLI still parses every other answer and one from an
815
+ // API before it.
816
+ released: z.literal(true).optional(),
512
817
  });
513
818
  // Claim and submit payloads name the task they are for, and the server checks
514
819
  // it against the path. Without it a signed claim for one task could be
@@ -534,6 +839,18 @@ export const SubmitTaskRequest = z.strictObject({
534
839
  .string()
535
840
  .refine((text) => utf8Encode(JSON.stringify(text)).length <= MAX_SUBMISSION_BYTES, `submission must be at most ${MAX_SUBMISSION_BYTES} bytes as JSON`),
536
841
  fingerprint: Fingerprint.optional(),
842
+ // The name of the model that solved the task, as the agent says
843
+ // (VOU-615), ModelName in ./model-name.ts. The API stores it once with
844
+ // the submission and nothing reads it as proof, so it earns nothing.
845
+ // Absent means not said, as from a CLI from before it.
846
+ modelName: ModelName.optional(),
847
+ });
848
+ // POST /v1/tasks/:id/release, the claimant giving its claim back
849
+ // (VOU-572). Named for its task like claim and submit, checked against the
850
+ // path. A captured envelope cannot end a later claim, since the release
851
+ // bars its agent from the task.
852
+ export const ReleaseTaskRequest = z.strictObject({
853
+ taskId: z.uuid(),
537
854
  });
538
855
  // Named for its task like claim and submit, checked against the path.
539
856
  // origin is the reporting side's, TaskOrigin above.
@@ -547,8 +864,7 @@ export const TaskOutcomeRequest = z.strictObject({
547
864
  // POST /v1/tasks/:id/submission, the poster's signed read of its task with
548
865
  // the submission and both outcome reports. The public GET leaves the
549
866
  // submission out. issuedAt bounds how long a captured envelope could be sent
550
- // again, the same window as rename.
551
- export const TASK_READ_MAX_AGE_SEC = RENAME_MAX_AGE_SEC;
867
+ // again (SIGNED_REQUEST_MAX_AGE_SEC).
552
868
  export const TaskSubmissionRequest = z.strictObject({
553
869
  taskId: z.uuid(),
554
870
  issuedAt: Timestamp,
@@ -605,7 +921,8 @@ export const ListTasksQuery = z.strictObject({
605
921
  // less the tasks this agent is barred from after its failed submits. Which
606
922
  // tasks those are is the agent's own business, so it is a signed read and
607
923
  // the public GET stays as it is. issuedAt bounds how long a captured
608
- // envelope could be sent again, the window of the submission read.
924
+ // envelope could be sent again, the window of every signed request
925
+ // (SIGNED_REQUEST_MAX_AGE_SEC).
609
926
  export const OpenTasksRequest = z.strictObject({
610
927
  taskType: TaskType.optional(),
611
928
  seed: z.boolean().optional(),
@@ -619,15 +936,209 @@ export const ListTasksResponse = z.strictObject({
619
936
  tasks: z.array(TaskResponse),
620
937
  nextCursor: z.string().nullable(),
621
938
  });
939
+ /*
940
+ * Duels (VOU-475, GAME-8, D-GAME-4, D-GAME-7), under /v1/duels. Every
941
+ * agent route is signed with the envelope of the other game routes, body
942
+ * SignedGameRequest { envelope }, and each payload carries issuedAt, which
943
+ * bounds how long a captured envelope could be sent again
944
+ * (SIGNED_REQUEST_MAX_AGE_SEC). A write names the seek or duel it was signed
945
+ * for, checked against the path. The reads are POSTs, as every signed read
946
+ * is, since a GET has no body for the envelope.
947
+ */
948
+ // POST /v1/duels/seek, an open call for a duel in one category.
949
+ export const SeekDuelRequest = z.strictObject({
950
+ category: TaskCategory,
951
+ issuedAt: Timestamp,
952
+ });
953
+ // DELETE /v1/duels/seek/:id, the agent cancels its own open seek.
954
+ export const CancelSeekRequest = z.strictObject({
955
+ seekId: z.uuid(),
956
+ issuedAt: Timestamp,
957
+ });
958
+ // POST /v1/duels/challenge, an invite to one agent by id or handle
959
+ // slug/name, as a post's assignee is named.
960
+ export const ChallengeDuelRequest = z.strictObject({
961
+ opponent: AgentRef,
962
+ category: TaskCategory,
963
+ issuedAt: Timestamp,
964
+ });
965
+ // POST /v1/duels/:id/rematch, /accept and /decline, named for the duel.
966
+ export const DuelActionRequest = z.strictObject({
967
+ duelId: z.uuid(),
968
+ issuedAt: Timestamp,
969
+ });
970
+ // POST /v1/duels/mine, the agent's own duels in one state, active by
971
+ // default, from both sides, newest first on (created_at, id), paged with
972
+ // the task list's cursor.
973
+ export const ListDuelsRequest = z.strictObject({
974
+ state: DuelState.default('active'),
975
+ limit: Limit,
976
+ cursor: TasksCursorParam.optional(),
977
+ issuedAt: Timestamp,
978
+ });
979
+ // POST /v1/duels/inbox, the invites to the agent still waiting for its
980
+ // answer, newest first, paged the same way.
981
+ export const DuelInboxRequest = z.strictObject({
982
+ limit: Limit,
983
+ cursor: TasksCursorParam.optional(),
984
+ issuedAt: Timestamp,
985
+ });
986
+ // GET /v1/me/agents/:id/duels (VOU-581), the operator's read of one of its
987
+ // agents' duels with the session cookie, paged as POST /mine. state is
988
+ // one POST /mine takes, active by default, except invited, which is the
989
+ // inbox, the invites to the agent still waiting for its answer.
990
+ export const MyDuelsQuery = z.strictObject({
991
+ state: DuelState.default('active'),
992
+ limit: Limit,
993
+ cursor: TasksCursorParam.optional(),
994
+ });
995
+ export const DuelParams = z.strictObject({ id: z.uuid() });
996
+ // One side of a duel. taskId is the side's own copy of the duel task, sent
997
+ // only in a signed answer to that side once the duel started, so no other
998
+ // agent can find and claim it or read its spec before the duel.
999
+ export const DuelSideView = z.strictObject({
1000
+ agentId: AgentId,
1001
+ handle: AgentHandle,
1002
+ taskId: z.uuid().optional(),
1003
+ });
1004
+ // A duel as GET /v1/duels/:id shows it to anyone, and the signed routes
1005
+ // to a side with its taskId. The timestamps and the result are null until
1006
+ // the duel reaches them. deadlineAt is the end of the 48 hour window.
1007
+ export const DuelResponse = z.strictObject({
1008
+ id: z.uuid(),
1009
+ category: TaskCategory,
1010
+ state: DuelState,
1011
+ origin: DuelOrigin,
1012
+ challenger: DuelSideView,
1013
+ opponent: DuelSideView,
1014
+ rematchOf: z.uuid().nullable(),
1015
+ invitedAt: Timestamp.nullable(),
1016
+ startedAt: Timestamp.nullable(),
1017
+ deadlineAt: Timestamp.nullable(),
1018
+ decidedAt: Timestamp.nullable(),
1019
+ result: DuelResult.nullable(),
1020
+ forfeit: z.boolean(),
1021
+ });
1022
+ // nextCursor is null on the last page.
1023
+ export const ListDuelsResponse = z.strictObject({
1024
+ duels: z.array(DuelResponse),
1025
+ nextCursor: z.string().nullable(),
1026
+ });
1027
+ // A seek as its agent sees it. duelId is the duel a match started.
1028
+ export const DuelSeekView = z.strictObject({
1029
+ id: z.uuid(),
1030
+ category: TaskCategory,
1031
+ state: DuelSeekState,
1032
+ expiresAt: Timestamp,
1033
+ duelId: z.uuid().nullable(),
1034
+ });
1035
+ // The answer of POST /v1/duels/seek and DELETE /v1/duels/seek/:id. duel
1036
+ // is the duel the seek started when it matched on the spot, with the
1037
+ // agent's taskId.
1038
+ export const SeekDuelResponse = z.strictObject({
1039
+ seek: DuelSeekView,
1040
+ duel: DuelResponse.optional(),
1041
+ });
1042
+ /*
1043
+ * Weekly challenges (VOU-479, GAME-12, D-GAME-11), under /v1/challenges.
1044
+ * The agent routes are signed like the duel routes, body SignedGameRequest
1045
+ * { envelope }, each payload with issuedAt within SIGNED_REQUEST_MAX_AGE_SEC.
1046
+ */
1047
+ // POST /v1/challenges/current, the agent's signed read of the current
1048
+ // week's challenge, and POST /v1/challenges/current/enter, its entry.
1049
+ export const ChallengeRequest = z.strictObject({ issuedAt: Timestamp });
1050
+ // Where one of an entrant's challenge tasks stands. submitted once its
1051
+ // one answer is in, correct true for a pass and false for a fail, and
1052
+ // null before.
1053
+ export const CHALLENGE_TASK_STATES = [
1054
+ 'unclaimed',
1055
+ 'claimed',
1056
+ 'submitted',
1057
+ ];
1058
+ export const ChallengeTaskState = z.enum(CHALLENGE_TASK_STATES);
1059
+ export const ChallengeTaskView = z.strictObject({
1060
+ taskId: z.uuid(),
1061
+ state: ChallengeTaskState,
1062
+ correct: z.boolean().nullable(),
1063
+ });
1064
+ // The current week's challenge as its agent sees it. entered says whether
1065
+ // the agent has an entry, and tasks are its own, empty without one. rank
1066
+ // is its live place, null until it has submitted.
1067
+ export const CurrentChallengeResponse = z.strictObject({
1068
+ isoWeek: IsoWeek,
1069
+ category: TaskCategory,
1070
+ closesAt: Timestamp,
1071
+ entered: z.boolean(),
1072
+ rank: z.int().min(1).nullable(),
1073
+ tasks: z.array(ChallengeTaskView).max(CHALLENGE_TASKS),
1074
+ });
1075
+ // GET /v1/challenges/:isoWeek/leaderboard, public, a week by its ISO week
1076
+ // or current for the week that holds now, at most limit rows.
1077
+ export const ChallengeBoardParams = z.strictObject({
1078
+ isoWeek: z.union([z.literal('current'), IsoWeek]),
1079
+ });
1080
+ export const ChallengeBoardQuery = z.strictObject({ limit: Limit });
1081
+ // One ranked entry, its place, the agent, its correct answers and their
1082
+ // total server time.
1083
+ export const ChallengeBoardRow = z.strictObject({
1084
+ rank: z.int().min(1),
1085
+ agent: z.strictObject({ agentId: AgentId, name: Name, handle: AgentHandle }),
1086
+ correct: z.int().min(0).max(CHALLENGE_TASKS),
1087
+ serverMs: z.int().min(0),
1088
+ });
1089
+ // The week's board, live while it is open and its final ranks once it
1090
+ // closed. entrants counts the entries with a submission, the ranked ones,
1091
+ // at most GAME.challengeRankMax.
1092
+ export const ChallengeBoardResponse = z.strictObject({
1093
+ isoWeek: IsoWeek,
1094
+ category: TaskCategory,
1095
+ state: ChallengeState,
1096
+ closesAt: Timestamp,
1097
+ entrants: z.int().min(0),
1098
+ rows: z.array(ChallengeBoardRow),
1099
+ });
1100
+ // One of the operator's agents with an entry in the week, its rank and its
1101
+ // tasks as CurrentChallengeResponse shows them to the agent. rank is the
1102
+ // live place while the week is open and the final one once it closed, null
1103
+ // for an entry with no submission.
1104
+ export const MyChallengeEntry = z.strictObject({
1105
+ agentId: AgentId,
1106
+ rank: z.int().min(1).nullable(),
1107
+ tasks: z.array(ChallengeTaskView).max(CHALLENGE_TASKS),
1108
+ });
1109
+ // GET /v1/me/challenges/:isoWeek (VOU-584), the signed in operator's
1110
+ // entries in one week, current or an ISO week as the board takes it, one
1111
+ // per agent with an entry, ordered by agent name, empty with none.
1112
+ export const MyChallengeResponse = z.strictObject({
1113
+ isoWeek: IsoWeek,
1114
+ category: TaskCategory,
1115
+ state: ChallengeState,
1116
+ closesAt: Timestamp,
1117
+ entries: z.array(MyChallengeEntry),
1118
+ });
622
1119
  // The public task views the web shows, GET /v1/tasks/board, GET
623
1120
  // /v1/tasks/:id/view and GET /v1/agents/:id/tasks. TaskResponse above is
624
1121
  // the CLI's shape. These carry the two sides as handles and never a
625
1122
  // submission. verification is always the public spec.
1123
+ /*
1124
+ * Which tasks the board lists (VOU-569), from the task's timestamps as
1125
+ * taskState reads them, since no column holds a state. open is a live task
1126
+ * nobody claimed. claimed is a live claim, submitted or not, so it keeps
1127
+ * the TaskState values claimed and submitted. verified is every verified
1128
+ * task, newest verified first. all is every task, expired ones included.
1129
+ * The others list newest posted first.
1130
+ */
1131
+ export const TaskBoardState = z.enum(['open', 'claimed', 'verified', 'all']);
626
1132
  // GET /v1/tasks/board. Open tasks, newest first, addressed ones included
627
- // with their assignee.
1133
+ // with their assignee, unless state asks for others. A cursor pages the
1134
+ // state it came from.
1135
+ // category filters stored tasks, so it takes any stored category.
1136
+ // minDifficulty keeps the tasks at that difficulty or above, 3, 4 or 5.
628
1137
  export const TaskBoardQuery = z.strictObject({
629
1138
  taskType: TaskType.optional(),
630
- category: TaskCategory.optional(),
1139
+ category: StoredTaskCategory.optional(),
1140
+ state: TaskBoardState.default('open'),
1141
+ minDifficulty: MinDifficultyParam.optional(),
631
1142
  limit: Limit,
632
1143
  cursor: TasksCursorParam.optional(),
633
1144
  });
@@ -652,6 +1163,38 @@ export const HoldReason = z.enum([
652
1163
  'operator_ban',
653
1164
  'key_compromise',
654
1165
  ]);
1166
+ /*
1167
+ * Why a verified task that counts adds less than its weight, or nothing,
1168
+ * named after the step of counted evidence that cut it (VOU-139, VOU-296,
1169
+ * SEAL standard section 4). daily_ceiling when
1170
+ * COUNTED_EVIDENCE.dailyCeiling heavier, more valuable or earlier tasks of
1171
+ * the claimant counted that UTC day, so it adds nothing. diminishing when
1172
+ * it is past the first COUNTED_EVIDENCE.diminishingK of its category (its
1173
+ * seed task type, or its poster's operator). pass_rate when its type
1174
+ * weighed less than 1 on the day it verified (tasks.type_weight).
1175
+ * pair_curve when it is past the first COUNTED_EVIDENCE.pairCurve.free
1176
+ * tasks of its operator pair in the pair window. share_cap when the share
1177
+ * cap took part or all of it. outside_window when it was verified before
1178
+ * the 180 day window. When more than one of diminishing, pass_rate,
1179
+ * pair_curve and share_cap cut a task, the note names the one that kept
1180
+ * the smallest share of what reached it, which is the one that took the
1181
+ * most, the later in the standard's order on a tie. The category curve
1182
+ * counts as a cut only past the first COUNTED_EVIDENCE.diminishingK, so
1183
+ * below that place a later step names the note even when the curve took
1184
+ * more. The scoring run
1185
+ * decides every note but outside_window and stores it on the task
1186
+ * (tasks.counted_note, whose check constraint lists the same values but
1187
+ * outside_window). The released web shows no line for a note it does not
1188
+ * know, so a new value needs no new web first.
1189
+ */
1190
+ export const CountedNote = z.enum([
1191
+ 'daily_ceiling',
1192
+ 'diminishing',
1193
+ 'pass_rate',
1194
+ 'pair_curve',
1195
+ 'share_cap',
1196
+ 'outside_window',
1197
+ ]);
655
1198
  // One side of a task, as it is now.
656
1199
  export const TaskParty = z.strictObject({
657
1200
  id: AgentId,
@@ -667,6 +1210,13 @@ export const TaskParty = z.strictObject({
667
1210
  // (TAKER-5). The taker is only ever a claimant.
668
1211
  operatedBySealKeeper: z.boolean(),
669
1212
  });
1213
+ // What a task can earn, TaskView.worth. min is a routine pass, at the
1214
+ // credit a counterparty task's poster below silver pays (VOU-574), and max
1215
+ // the most any claimant can get at the difficulty the poster gave.
1216
+ export const TaskWorth = z.strictObject({
1217
+ min: z.int().min(0),
1218
+ max: z.int().min(0),
1219
+ });
670
1220
  export const TaskView = z.strictObject({
671
1221
  id: z.uuid(),
672
1222
  taskType: TaskType,
@@ -680,8 +1230,9 @@ export const TaskView = z.strictObject({
680
1230
  // The one agent that can claim an addressed task. Null for an open task,
681
1231
  // and after the assignee agent was deleted.
682
1232
  assignee: TaskParty.nullable(),
683
- // Where the post came from, manual when the poster's CLI did not say.
684
- origin: TaskOrigin,
1233
+ // Where the post came from, manual when the poster's CLI did not say,
1234
+ // duel or challenge for a game task the server made.
1235
+ origin: StoredTaskOrigin,
685
1236
  ...realTaskFields,
686
1237
  // The scoring rule. True when the poster is the seed agent or an agent of
687
1238
  // another operator than the claimant, false when both sides share an
@@ -699,15 +1250,30 @@ export const TaskView = z.strictObject({
699
1250
  // false, null when it is null, and null for a task verified since its
700
1251
  // claimant's last scoring run, until that run.
701
1252
  counted: z.number().min(0).max(1).nullable(),
702
- // Why a task that counts adds less than its weight, or nothing.
703
- // daily_ceiling when COUNTED_EVIDENCE.dailyCeiling heavier, more valuable
704
- // or earlier tasks of the claimant counted that UTC day, diminishing when
705
- // it is past the first COUNTED_EVIDENCE.diminishingK of its category (its
706
- // seed task type, or its poster's operator), outside_window when it was
707
- // verified before the 180 day window. Null otherwise.
708
- countedNote: z
709
- .enum(['daily_ceiling', 'diminishing', 'outside_window'])
710
- .nullable(),
1253
+ // Why a task that counts adds less than its weight, or nothing
1254
+ // (CountedNote), as the last scoring run decided it. Null otherwise.
1255
+ countedNote: CountedNote.nullable(),
1256
+ // The base credit the task earned as it verified (VOU-498), 10 times the
1257
+ // multiplier of its difficulty times the novelty bonus (TRUST_CREDIT),
1258
+ // before the counted value and the decay, so not what it adds to Trust
1259
+ // Score. Null while unverified, for a task that counts for nothing and
1260
+ // for one verified before the issuer stored it. Optional so the web
1261
+ // still reads an answer from an older API.
1262
+ baseCredit: z.number().min(0).nullable().optional(),
1263
+ // What the task can earn, whole numbers of Trust (VOU-569, taskWorth in
1264
+ // the API). min is a routine pass, its difficulty's base credit with no
1265
+ // novelty bonus. max is the same with the most novelty bonus a claimant
1266
+ // can get at that difficulty, the one of an agent whose average is 1.
1267
+ // Both at a counted value of 1 and before any decay. It names no agent,
1268
+ // so it is the same for every reader. Optional so the web still reads an
1269
+ // answer from an older API.
1270
+ worth: TaskWorth.optional(),
1271
+ // The Trust the task added as its claimant's last scoring run counted
1272
+ // it, its base credit times its counted value, the amount the roll-up
1273
+ // put on the claimant's day, rounded to a whole number. Absent until the
1274
+ // run stored a counted value, and while counted is null. Optional so the
1275
+ // web still reads an answer from an older API.
1276
+ earned: z.int().min(0).optional(),
711
1277
  // The reason class of a void in force on the task (COL-5), which makes it
712
1278
  // weigh 0 and count for nothing on either side. Absent without one. Never
713
1279
  // the note. Optional so the web still reads an answer from an older API.
@@ -717,12 +1283,127 @@ export const TaskView = z.strictObject({
717
1283
  submittedAt: Timestamp.nullable(),
718
1284
  verifiedAt: Timestamp.nullable(),
719
1285
  expiresAt: Timestamp,
1286
+ // When each side of a counterparty task first reported its outcome, null
1287
+ // until it reports, and always null for a hash or schema task. A later
1288
+ // report replaces the outcome and keeps this time. Never the outcome
1289
+ // itself. Optional so the web still reads an answer from an older API.
1290
+ posterReportedAt: Timestamp.nullable().optional(),
1291
+ claimantReportedAt: Timestamp.nullable().optional(),
1292
+ // When the poster of a counterparty task let its response time lapse and
1293
+ // the claimant's success report stood, which verified the task (POST-4),
1294
+ // so it equals verifiedAt. Null otherwise. Optional so the web still
1295
+ // reads an answer from an older API.
1296
+ posterLapsedAt: Timestamp.nullable().optional(),
1297
+ // How many claims on the task ended before it verified and gave it back,
1298
+ // released by their claimant (VOU-572) or ended at the failed submit
1299
+ // cap, from the issuer's own record. 0 when none. Optional so the web
1300
+ // still reads an answer from an older API.
1301
+ releasedClaims: z.number().int().min(0).optional(),
720
1302
  });
721
1303
  // nextCursor is null on the last page.
722
1304
  export const TaskViewsResponse = z.strictObject({
723
1305
  tasks: z.array(TaskView),
724
1306
  nextCursor: z.string().nullable(),
725
1307
  });
1308
+ /*
1309
+ * One number a block moved (VOU-573), as the scoring run had stored it when
1310
+ * the block opened (before) and once it could take no more tasks (after).
1311
+ * before is absent when it could not be read as the block opened, and a
1312
+ * rank either side when the agent had no place on the board then.
1313
+ */
1314
+ export const BlockMove = z.strictObject({
1315
+ before: z.int().min(0).optional(),
1316
+ after: z.int().min(0).optional(),
1317
+ });
1318
+ /*
1319
+ * What a block moved (VOU-573, UI project), written once when the block
1320
+ * could take no more tasks and never changed, so every read answers the
1321
+ * same. trust is the agent's Trust Score, whole, rank its place on the
1322
+ * weekly Trust board across all categories of the week the block started,
1323
+ * and streak its day streak. No race lead, since there is no Race yet.
1324
+ */
1325
+ export const BlockMoved = z.strictObject({
1326
+ trust: BlockMove,
1327
+ rank: BlockMove,
1328
+ streak: BlockMove,
1329
+ });
1330
+ /*
1331
+ * Blocks (D-UI-9, VOU-550, BLOCK in ./blocks.ts), a run of verified tasks
1332
+ * by one agent in one category, close together in time. GET
1333
+ * /v1/blocks/:id, GET /v1/agents/:id/blocks and GET /v1/blocks/latest.
1334
+ * Every time on a block is a server time.
1335
+ */
1336
+ export const BlockView = z.strictObject({
1337
+ id: z.uuid(),
1338
+ // The agent whose work it is, as it is now.
1339
+ agent: TaskParty,
1340
+ category: StoredTaskCategory,
1341
+ // blockTitle of startedAt (./blocks.ts), worked out by the API, never
1342
+ // stored. Any text, so a band added later reads in an older client.
1343
+ title: z.string().min(1),
1344
+ // The verified_at of its first task and of its latest.
1345
+ startedAt: Timestamp,
1346
+ endedAt: Timestamp,
1347
+ taskCount: z.int().min(1).max(BLOCK.maxTasks),
1348
+ // The base credit its tasks stored as they verified (TaskView.baseCredit),
1349
+ // summed, before the counted value and the decay, so not what the block
1350
+ // adds to Trust Score.
1351
+ creditTotal: z.number().min(0),
1352
+ difficultySum: z.int().min(1),
1353
+ // Rejections of the agent's work in the block's category submitted while
1354
+ // it was open, less those the poster withdrew.
1355
+ rejectedCount: z.int().min(0),
1356
+ // The handles of the posters who confirmed its tasks, each once, in the
1357
+ // order a task of theirs first joined, at most BLOCK.confirmers
1358
+ // (VOU-583). A poster confirms a task with the confirm check it did not
1359
+ // let lapse, so a hash or schema task, which the server checked, and a
1360
+ // task that stood on the claimant's report add no one. Stored as its
1361
+ // tasks join and shown by each poster's handle as it is now. Optional so
1362
+ // the web still reads an answer from an older API.
1363
+ confirmedBy: z.array(AgentHandle).max(BLOCK.confirmers).optional(),
1364
+ // What the block moved (VOU-573), absent until it can take no more tasks
1365
+ // and on a block from before it was stored. Optional, so an answer from
1366
+ // an older API parses.
1367
+ moved: BlockMoved.optional(),
1368
+ // On GET /v1/blocks/latest only, the agent's numbers for the live hero
1369
+ // card (VOU-573), from the stored trust_days rows and the agent row.
1370
+ // streak is its day streak, dayTasks its verified tasks a day over the
1371
+ // last TRUST_SCORE.seriesDays UTC days oldest first, today last, and
1372
+ // monthTasks and monthTrust their totals, Trust whole, before the decay.
1373
+ // Optional, so an answer from an older API parses.
1374
+ streak: z.int().min(0).optional(),
1375
+ dayTasks: z.array(z.int().min(0)).length(TRUST_SCORE.seriesDays).optional(),
1376
+ monthTasks: z.int().min(0).optional(),
1377
+ monthTrust: z.int().min(0).optional(),
1378
+ });
1379
+ export const BlockParams = z.strictObject({ id: z.uuid() });
1380
+ // GET /v1/blocks/:id. The block and its tasks in the order they verified,
1381
+ // at most BLOCK.maxTasks. Each task names its poster and its check method,
1382
+ // so a reader sees who confirmed it, the server for a hash or schema task
1383
+ // and the poster for a counterparty one, or no one when the poster let its
1384
+ // response time lapse (posterLapsedAt).
1385
+ export const BlockResponse = z.strictObject({
1386
+ block: BlockView,
1387
+ tasks: z.array(TaskView),
1388
+ });
1389
+ // GET /v1/agents/:id/blocks, newest started first, paged on
1390
+ // (started_at, id) with the cursor from nextCursor, the task list cursor.
1391
+ export const AgentBlocksQuery = z.strictObject({
1392
+ limit: Limit,
1393
+ cursor: TasksCursorParam.optional(),
1394
+ });
1395
+ // nextCursor is null on the last page.
1396
+ export const AgentBlocksResponse = z.strictObject({
1397
+ blocks: z.array(BlockView),
1398
+ nextCursor: z.string().nullable(),
1399
+ });
1400
+ // GET /v1/blocks/latest takes no query parameters.
1401
+ export const LatestBlocksQuery = z.strictObject({});
1402
+ // GET /v1/blocks/latest. The blocks most recently added to on the network,
1403
+ // newest first, at most BLOCK.latest.
1404
+ export const LatestBlocksResponse = z.strictObject({
1405
+ blocks: z.array(BlockView).max(BLOCK.latest),
1406
+ });
726
1407
  // The body of a signed rating. A rating payload is small, so it takes the
727
1408
  // event envelope cap.
728
1409
  export const SignedRatingRequest = z.strictObject({
@@ -773,6 +1454,208 @@ export const ScoreResponse = z.strictObject({
773
1454
  agentId: AgentId,
774
1455
  scores: z.array(ScoreEntry),
775
1456
  });
1457
+ // An agent's place on the Trust leaderboards (TS-7, VOU-502), each counted
1458
+ // through the board's index (TS-11) up to TRUST_SCORE.rankMax, 1 at the
1459
+ // top. Absent without a row on the board or past rankMax. week is this UTC
1460
+ // week's board and weekChange the places moved against last week's, up
1461
+ // positive, absent without a rank on both.
1462
+ export const TrustRanks = z.strictObject({
1463
+ week: z.int().min(1).optional(),
1464
+ weekChange: z.int().optional(),
1465
+ allTime: z.int().min(1).optional(),
1466
+ });
1467
+ // One category of the current version's Trust Score, as the scoring run
1468
+ // stored it (trust_categories). tasks is its verified tasks that count,
1469
+ // weighed and rounded down as verified_tasks is, which silver's
1470
+ // categories read.
1471
+ export const TrustCategoryView = z.strictObject({
1472
+ category: StoredTaskCategory,
1473
+ trust: z.int().min(0),
1474
+ tasks: z.int().min(0),
1475
+ rank: TrustRanks.omit({ weekChange: true }).optional(),
1476
+ });
1477
+ // One UTC day of the series, what the day's verified tasks added on any
1478
+ // version, each task's base credit times its counted value with no decay,
1479
+ // as the scoring run last saw them (trust_days). A day with no task is 0,
1480
+ // 0 and a null difficulty, the mean of the day's tasks otherwise.
1481
+ export const TrustDayView = z.strictObject({
1482
+ date: z.iso.date(),
1483
+ trust: z.int().min(0),
1484
+ tasks: z.int().min(0),
1485
+ difficulty: z.number().min(1).max(5).nullable(),
1486
+ });
1487
+ /*
1488
+ * GET /v1/agents/:id/trust (TS-7, VOU-502). Public. The rows the scoring
1489
+ * run stored, never a sum of tasks. Whole numbers of Trust, rounded.
1490
+ *
1491
+ * trust is the Trust Score of the current version (standing.trust_score),
1492
+ * 0 for an agent with no verified task, before its first run and on a row
1493
+ * written before Trust Score was stored. delta7d is the Trust the last
1494
+ * TRUST_SCORE.deltaDays days of days added, today included, before
1495
+ * penalties and before the fade of older work, so it is never below 0.
1496
+ * categories are the version's, highest first, empty with no verified
1497
+ * task. days are the last TRUST_SCORE.seriesDays UTC days, oldest first,
1498
+ * each there with zeros when nothing verified. operator is the agent's
1499
+ * operator's number (operator_trust), absent before a run wrote it, with
1500
+ * change7d (VOU-582), that number now less the same roll up over its
1501
+ * agents' Trust seven days back, each agent's Trust less its delta7d from
1502
+ * the same trust_days rows, absent when none of its agents has a day of
1503
+ * trust_days before those seven, so there is no Trust seven days back to
1504
+ * change from. held
1505
+ * is true while a hold withholds the SEAL (holds.ts), read live as the
1506
+ * agent answer reads it, and the numbers are shown as they are.
1507
+ */
1508
+ export const TrustResponse = z.strictObject({
1509
+ agentId: AgentId,
1510
+ version: StoredVersion,
1511
+ trust: z.int().min(0),
1512
+ delta7d: z.int(),
1513
+ rank: TrustRanks.optional(),
1514
+ categories: z.array(TrustCategoryView).max(STORED_TASK_CATEGORIES.length),
1515
+ days: z.array(TrustDayView).length(TRUST_SCORE.seriesDays),
1516
+ operator: z
1517
+ .strictObject({
1518
+ trust: z.int().min(0),
1519
+ agents: z.int().min(0),
1520
+ change7d: z.int().min(0).optional(),
1521
+ })
1522
+ .optional(),
1523
+ held: z.boolean().optional(),
1524
+ });
1525
+ /*
1526
+ * The models agents run and the changes they declare (VOU-551, UI-23).
1527
+ * Every number here is a report the nightly run stored. None of it moves a
1528
+ * Trust Score, a category score, stored credit or a level, and the model
1529
+ * names are what agents say about themselves, never proof.
1530
+ */
1531
+ // A model's key, modelKey of a declared name, as the stored rows hold it.
1532
+ const ModelKey = z.string().min(1).max(64);
1533
+ // One side of one category, as the run counted it. verified is the tasks
1534
+ // that count for Trust, failed the claims ended at the failed submit cap
1535
+ // or released after a failed submit (a clean release counts nowhere,
1536
+ // VOU-577), rejected the
1537
+ // rejection penalties, and credit the average stored base credit of the
1538
+ // verified ones, null when none stores one.
1539
+ export const ModelComparisonSideView = z.strictObject({
1540
+ verified: z.int().min(0),
1541
+ failed: z.int().min(0),
1542
+ rejected: z.int().min(0),
1543
+ credit: z.number().nullable(),
1544
+ });
1545
+ // One category of a comparison. Both sides and the verdict, or, below
1546
+ // MODEL_COMPARISON.minTasks verified tasks on either side, the two
1547
+ // verified counts alone and insufficient.
1548
+ export const ModelComparisonCategoryView = z.union([
1549
+ z.strictObject({
1550
+ before: ModelComparisonSideView,
1551
+ after: ModelComparisonSideView,
1552
+ verdict: z.enum(['better', 'worse', 'same']),
1553
+ }),
1554
+ z.strictObject({
1555
+ before: z.strictObject({ verified: z.int().min(0) }),
1556
+ after: z.strictObject({ verified: z.int().min(0) }),
1557
+ verdict: z.literal('insufficient'),
1558
+ }),
1559
+ ]);
1560
+ // A window of a comparison, from inclusive and to exclusive.
1561
+ const ModelWindowView = z.strictObject({ from: Timestamp, to: Timestamp });
1562
+ // One declared change of an agent's model. date is the UTC day of at, the
1563
+ // server's time when the change arrived. from and to are the names as the
1564
+ // agent declared them, null where it declared none, and the same name on
1565
+ // both sides for a change only the fingerprint's model part showed.
1566
+ // verdicts is each of the six categories' verdict from the stored
1567
+ // comparison, absent until the nightly run first compares the change.
1568
+ // final is true once the comparison's after window is
1569
+ // MODEL_COMPARISON.afterDays long. Until then a verdict reads the days
1570
+ // since the change so far, and one a later change replaced stays so, so a
1571
+ // page tells a partial verdict from a final one.
1572
+ export const ModelChangeView = z.strictObject({
1573
+ date: z.iso.date(),
1574
+ at: Timestamp,
1575
+ from: Name.nullable(),
1576
+ to: Name.nullable(),
1577
+ final: z.boolean(),
1578
+ verdicts: z.record(TaskCategory, ModelVerdict).optional(),
1579
+ });
1580
+ // GET /v1/agents/:id/model-changes. Public. The agent's declared changes,
1581
+ // newest first, at most MODEL_NETWORK.changes.
1582
+ export const ModelChangesResponse = z.strictObject({
1583
+ changes: z.array(ModelChangeView).max(MODEL_NETWORK.changes),
1584
+ });
1585
+ // GET /v1/agents/:id/model-changes/:date, the date a UTC day. The years
1586
+ // are bounded, since Postgres reads no year 0 and no day past 9999, and
1587
+ // no change is older than SealKeeper.
1588
+ export const ModelChangeParams = z.strictObject({
1589
+ id: AgentId,
1590
+ date: z.iso
1591
+ .date()
1592
+ .refine((d) => d >= '2026-01-01' && d <= '9998-12-31', 'A date from 2026 to 9998'),
1593
+ });
1594
+ /*
1595
+ * GET /v1/agents/:id/model-changes/:date. Public. The agent's newest
1596
+ * change of that UTC day with its comparison as the nightly run stored
1597
+ * it. window and categories are absent until the run first compares the
1598
+ * change, and categories holds all six. final is true once the after
1599
+ * window is MODEL_COMPARISON.afterDays long. The run compares only an
1600
+ * agent's newest change, so one a later change replaced keeps the
1601
+ * comparison it had and may stay not final.
1602
+ */
1603
+ export const ModelChangeResponse = z.strictObject({
1604
+ date: z.iso.date(),
1605
+ at: Timestamp,
1606
+ from: Name.nullable(),
1607
+ to: Name.nullable(),
1608
+ final: z.boolean(),
1609
+ window: z
1610
+ .strictObject({ before: ModelWindowView, after: ModelWindowView })
1611
+ .optional(),
1612
+ categories: z.record(TaskCategory, ModelComparisonCategoryView).optional(),
1613
+ });
1614
+ // How many agents of a network event read better, worse or same in one
1615
+ // category. An insufficient comparison counts in none.
1616
+ export const ModelVerdictCountsView = z.strictObject({
1617
+ better: z.int().min(0),
1618
+ worse: z.int().min(0),
1619
+ same: z.int().min(0),
1620
+ });
1621
+ const ModelVerdictsView = z.record(TaskCategory, ModelVerdictCountsView);
1622
+ // A model agents run now. name is the newest declared name of the key and
1623
+ // firstSeen the UTC day it was first seen. operators is the distinct
1624
+ // operators of its agents (VOU-578), optional so the web still reads an
1625
+ // answer from an older API. verdicts is the latest network event to it,
1626
+ // present only when the model has MODEL_NETWORK.minAgents agents or more
1627
+ // and such an event exists.
1628
+ export const ModelView = z.strictObject({
1629
+ key: ModelKey,
1630
+ name: Name,
1631
+ agents: z.int().min(1),
1632
+ operators: z.int().min(1).optional(),
1633
+ firstSeen: z.iso.date(),
1634
+ verdicts: ModelVerdictsView.optional(),
1635
+ });
1636
+ // A change of model that MODEL_NETWORK.minAgents agents or more, of
1637
+ // minOperators operators or more, made in the UTC week from weekStart, a
1638
+ // Monday. agents and the verdicts count at most perOperator agents of one
1639
+ // operator (VOU-578). from and to are the two keys, which the models list names.
1640
+ export const ModelEventView = z.strictObject({
1641
+ from: ModelKey,
1642
+ to: ModelKey,
1643
+ weekStart: z.iso.date(),
1644
+ agents: z.int().min(1),
1645
+ operators: z.int().min(1),
1646
+ verdicts: ModelVerdictsView,
1647
+ });
1648
+ // GET /v1/models. Public. The models agents run now, most operators
1649
+ // first, then most agents, then by key, at most MODEL_NETWORK.models, and the latest network
1650
+ // events, newest week first, at most MODEL_NETWORK.events. Both are the
1651
+ // nightly roll ups, so up to a day old. unnamed is how many agents have
1652
+ // named no model, from the same roll up (VOU-583), absent until its first
1653
+ // night. Optional so the web still reads an answer from an older API.
1654
+ export const ModelsResponse = z.strictObject({
1655
+ models: z.array(ModelView).max(MODEL_NETWORK.models),
1656
+ events: z.array(ModelEventView).max(MODEL_NETWORK.events),
1657
+ unnamed: z.int().min(0).optional(),
1658
+ });
776
1659
  // The signed in operator and the agents they own, for the web's my agents
777
1660
  // page. lastSeenAt is the newest received_at of the agent's events, null
778
1661
  // when it has sent none. Agents are ordered by lastSeenAt, newest first,
@@ -792,6 +1675,9 @@ export const MeAgent = z.strictObject({
792
1675
  level: Level.optional(),
793
1676
  standing: Standing.optional(),
794
1677
  runtime: Runtime,
1678
+ // The agent's game settings (VOU-469), for the web's game switch.
1679
+ // Optional, as every field added to an existing answer.
1680
+ game: GameSettingsView.optional(),
795
1681
  });
796
1682
  // The signed in operator. createdAt is when the operator first registered
797
1683
  // or signed in, shown as operator since on the account page.
@@ -831,16 +1717,30 @@ export const DeleteMeRequest = z.strictObject({
831
1717
  });
832
1718
  // GET /v1/agents/:id/seal and its alias /credential. seal and credential are
833
1719
  // the same compact JWS. credential is kept for one release (VOU-77).
1720
+ // payload is the version the issuer writes, 1 or 3 (IssuedSealPayload).
834
1721
  export const CredentialResponse = z.strictObject({
835
1722
  credential: Jws,
836
1723
  seal: Jws,
837
- payload: CredentialPayload,
838
- });
839
- // A bare /v1/leaderboard is the reliability board.
840
- export const LeaderboardQuery = z.strictObject({
1724
+ payload: IssuedSealPayload,
1725
+ });
1726
+ // The two Trust leaderboards (TS-11, VOU-506). week ranks the credit
1727
+ // earned in the current UTC week from Monday 00:00 UTC, all ranks Trust
1728
+ // Score.
1729
+ export const TrustPeriod = z.enum(['week', 'all']);
1730
+ // A dimension board, or with period a Trust board (TS-11), across every
1731
+ // category or in the one category given. A bare /v1/leaderboard is the
1732
+ // reliability board, as before. The two are strict, so a query cannot mix
1733
+ // a dimension with a period or a category.
1734
+ export const DimensionBoardQuery = z.strictObject({
841
1735
  dimension: Dimension.default('reliability'),
842
1736
  limit: Limit,
843
1737
  });
1738
+ export const TrustBoardQuery = z.strictObject({
1739
+ period: TrustPeriod,
1740
+ category: StoredTaskCategory.optional(),
1741
+ limit: Limit,
1742
+ });
1743
+ export const LeaderboardQuery = z.union([DimensionBoardQuery, TrustBoardQuery]);
844
1744
  // Rows on the current version only, ranked by level first (gold, silver,
845
1745
  // bronze, then none or not yet scored), then by value, highest first. Ties
846
1746
  // go to the value computed first.
@@ -873,6 +1773,48 @@ export const LeaderboardResponse = z.strictObject({
873
1773
  dimension: Dimension,
874
1774
  entries: z.array(LeaderboardEntry),
875
1775
  });
1776
+ // One agent on a Trust board, as on a dimension board, with trust, the
1777
+ // number it ranks on, where a dimension board has value. On the all time
1778
+ // board its current version's Trust Score, and on a weekly board the
1779
+ // credit its verified tasks of the week added on any version, each task's
1780
+ // base credit times its counted value, before penalties and decay. Both
1781
+ // from the rows the scoring run stored (trust_boards), rounded to whole
1782
+ // Trust. rankChange, on a weekly board only (VOU-570, UI-36), is the
1783
+ // places the agent moved against its final rank on the same board last
1784
+ // week (week_ranks), up positive, 0 for none, and absent when it had no
1785
+ // rank last week, so the page can show it as new. Optional so an answer
1786
+ // from an older API parses.
1787
+ export const TrustLeaderboardEntry = LeaderboardEntry.omit({
1788
+ value: true,
1789
+ }).extend({ trust: z.int().min(0), rankChange: z.int().optional() });
1790
+ // How many of the signed in operator's agents a Trust board shows beside
1791
+ // its entries (VOU-552, UI-12).
1792
+ export const BOARD_MINE_MAX = 5;
1793
+ // One of the signed in operator's agents on the Trust board read, its
1794
+ // rank counted as the trust answer counts one (TS-7), absent past
1795
+ // TRUST_SCORE.rankMax, and trust the number it ranks on, rounded.
1796
+ export const TrustBoardMine = z.strictObject({
1797
+ agentId: AgentId,
1798
+ name: Name,
1799
+ handle: AgentHandle,
1800
+ rank: z.int().min(1).optional(),
1801
+ trust: z.int().min(0),
1802
+ });
1803
+ // GET /v1/leaderboard?period=, highest first, ties to the lower agent id,
1804
+ // at most limit entries. A weekly board starts empty at Monday 00:00 UTC
1805
+ // and fills as the scoring run scores the agents that verify work.
1806
+ // weekStart is the Monday of the week, YYYY-MM-DD, on a weekly board. An
1807
+ // agent with no Trust in the period is not listed. mine is sent only to a
1808
+ // caller with a session (VOU-552), the operator's agents on this board,
1809
+ // highest first, at most BOARD_MINE_MAX, empty when none is on it, and
1810
+ // such an answer is private and never cached.
1811
+ export const TrustLeaderboardResponse = z.strictObject({
1812
+ period: TrustPeriod,
1813
+ category: StoredTaskCategory.optional(),
1814
+ weekStart: z.iso.date().optional(),
1815
+ entries: z.array(TrustLeaderboardEntry),
1816
+ mine: z.array(TrustBoardMine).max(BOARD_MINE_MAX).optional(),
1817
+ });
876
1818
  // The public feed. Payloads carry public facts only. Never an operator
877
1819
  // email, an envelope, a spec or a submission.
878
1820
  export const FeedKind = z.enum([
@@ -887,6 +1829,29 @@ export const FeedKind = z.enum([
887
1829
  // The issuer withheld the agent's SEAL for cause (VOU-85). Written when a
888
1830
  // hold is placed, one per agent it covers.
889
1831
  'refusal',
1832
+ // The agent declared another model (VOU-551). Written when a change of
1833
+ // model name is recorded.
1834
+ 'model_change',
1835
+ // The agent's streak reached one of STREAK_MILESTONES (VOU-472). Written
1836
+ // by the scoring run, once per milestone per streak.
1837
+ 'streak_milestone',
1838
+ // An agent challenged another to a duel, or asked for a rematch
1839
+ // (VOU-475). Written with the invite.
1840
+ 'duel_invited',
1841
+ // A duel started, an invite accepted or two seeks matched (VOU-475).
1842
+ // Written with the duel's tasks.
1843
+ 'duel_started',
1844
+ // A duel finished with a result (VOU-476). Written as it is decided. An
1845
+ // aborted duel writes none.
1846
+ 'duel_finished',
1847
+ // A weekly challenge opened (VOU-479). Written by the sweep with the
1848
+ // week's row.
1849
+ 'challenge_opened',
1850
+ // An entry that first reached the top places of its week's challenge
1851
+ // (VOU-479). Written by the submit that took it there, once per week.
1852
+ 'challenge_top10',
1853
+ // A weekly challenge that closed (VOU-479). Written by the close.
1854
+ 'challenge_closed',
890
1855
  ]);
891
1856
  // Why an agent was flagged. outcome_disagreement, the two sides of a
892
1857
  // counterparty task disagreed. taker_failures, the SealKeeper taker failed
@@ -902,12 +1867,270 @@ export const FeedMilestone = z.enum([
902
1867
  'verified_100',
903
1868
  'verified_1000',
904
1869
  ]);
1870
+ // The streaks the feed marks (VOU-472, D-GAME-8), in UTC days with a server
1871
+ // checked pass and no fail. The scoring run writes a streak_milestone item
1872
+ // when an agent's current streak reaches one, once per milestone per
1873
+ // streak, so again after a reset and regrowth. A streak is never evidence
1874
+ // and never in the SEAL.
1875
+ export const STREAK_MILESTONES = [7, 30, 100, 365];
1876
+ /*
1877
+ * GET /v1/me/dashboard (VOU-552, UI-24, D-UI-7). The parts of the
1878
+ * dashboard that no other answer carries, from rows the scoring run
1879
+ * stored, never a sum of tasks. Beside it the web reads the Trust Score,
1880
+ * the delta, the categories and the 30 day series from GET
1881
+ * /v1/agents/:id/trust and the level from the goal answer. Every part is
1882
+ * optional, so the web hides a card whose part is missing. Whole numbers
1883
+ * of Trust, rounded as the trust answer rounds them.
1884
+ */
1885
+ // How many agents the dashboard shows above and below the agent's rank on
1886
+ // this week's board.
1887
+ export const DASHBOARD_AROUND = 2;
1888
+ // agent is one of the signed in operator's agents. Without it the answer
1889
+ // is the operator's, every agent.
1890
+ export const DashboardQuery = z.strictObject({ agent: AgentId.optional() });
1891
+ // One UTC day of the week, what its verified tasks added (trust_days).
1892
+ export const DashboardDay = z.strictObject({
1893
+ date: z.iso.date(),
1894
+ trust: z.int().min(0),
1895
+ tasks: z.int().min(0),
1896
+ });
1897
+ // The UTC week that holds today, Monday 00:00 UTC to Sunday, as the weekly
1898
+ // board runs (trustWeekOf). weekStart is its Monday. tasks and trust are
1899
+ // its days' sums, difficulty the mean difficulty of its tasks, null with
1900
+ // none. days are its seven days, Monday first, the days still to come 0.
1901
+ export const DashboardWeek = z.strictObject({
1902
+ weekStart: z.iso.date(),
1903
+ tasks: z.int().min(0),
1904
+ trust: z.int().min(0),
1905
+ difficulty: z.number().min(1).max(5).nullable(),
1906
+ days: z.array(DashboardDay).length(7),
1907
+ });
1908
+ // The last TRUST_SCORE.seriesDays UTC days, today included, the same days
1909
+ // as the trust answer's series. emptyDays is how many had no task.
1910
+ export const DashboardLast30 = z.strictObject({
1911
+ tasks: z.int().min(0),
1912
+ trust: z.int().min(0),
1913
+ emptyDays: z.int().min(0).max(TRUST_SCORE.seriesDays),
1914
+ });
1915
+ // An agent next to this one on this week's board of every category, at
1916
+ // most DASHBOARD_AROUND above and below, highest first. gap is its trust
1917
+ // less this agent's, above 0 for one ahead.
1918
+ export const DashboardNeighbour = z.strictObject({
1919
+ agentId: AgentId,
1920
+ name: Name,
1921
+ handle: AgentHandle,
1922
+ rank: z.int().min(1),
1923
+ trust: z.int().min(0),
1924
+ gap: z.int(),
1925
+ });
1926
+ // The agent's bests as the scoring run moved them forward (agent_records).
1927
+ // bestDay is the most Trust one UTC day added. hardestTask is the task of
1928
+ // the highest difficulty that added credit, ties to the newest, date its
1929
+ // UTC day of verification. bestRank is the best place on the board of
1930
+ // every category of a closed UTC week, as the first run after the week
1931
+ // closed read it, weekStart that week's Monday. An open week never counts.
1932
+ // A best the agent has not set is absent. The longest streak is on the agent
1933
+ // answer, bestStreak (VOU-472).
1934
+ export const DashboardRecords = z.strictObject({
1935
+ bestDay: z
1936
+ .strictObject({ date: z.iso.date(), trust: z.int().min(0) })
1937
+ .optional(),
1938
+ hardestTask: z
1939
+ .strictObject({
1940
+ taskId: z.uuid(),
1941
+ difficulty: TaskDifficulty,
1942
+ category: StoredTaskCategory,
1943
+ date: z.iso.date(),
1944
+ // The task's type as the record stored it (VOU-583), absent on a
1945
+ // record from before migration 0079 whose task was gone. Optional so
1946
+ // the web still reads an answer from an older API.
1947
+ taskType: TaskType.optional(),
1948
+ })
1949
+ .optional(),
1950
+ bestRank: z
1951
+ .strictObject({ rank: z.int().min(1), weekStart: z.iso.date() })
1952
+ .optional(),
1953
+ });
1954
+ // A milestone the agent passed (agent_milestones), at when the scoring run
1955
+ // recorded it.
1956
+ export const DashboardFirst = z.strictObject({
1957
+ milestone: FeedMilestone,
1958
+ at: Timestamp,
1959
+ });
1960
+ // One of the operator's agents on the operator's dashboard. trust is the
1961
+ // Trust Score of its current version, whole, 0 before its first run, as
1962
+ // the trust answer's trust (VOU-582, UI-41). level is that version's
1963
+ // level, absent before its first run, as on the agent answer.
1964
+ // currentStreak is the agent answer's, 0 for an agent with no task. The
1965
+ // three are optional so an answer from an older API parses.
1966
+ export const DashboardAgent = z.strictObject({
1967
+ agentId: AgentId,
1968
+ name: Name,
1969
+ handle: AgentHandle,
1970
+ thisWeek: DashboardWeek.optional(),
1971
+ rank: TrustRanks.optional(),
1972
+ records: DashboardRecords.optional(),
1973
+ trust: z.int().min(0).optional(),
1974
+ level: Level.optional(),
1975
+ currentStreak: z.int().min(0).optional(),
1976
+ });
1977
+ /*
1978
+ * With agent, that agent's parts. thisWeek, last30 and firsts are always
1979
+ * sent, zeros and empty for an agent with no task, rank without a place on
1980
+ * any board is absent, around without a rank on this week's board, and
1981
+ * records before the agent set one. Without agent, the operator's. agents
1982
+ * holds each agent, at most the operator's cap, ordered by name, and
1983
+ * totals the week of all of them together.
1984
+ */
1985
+ export const DashboardResponse = z.strictObject({
1986
+ agentId: AgentId.optional(),
1987
+ thisWeek: DashboardWeek.optional(),
1988
+ last30: DashboardLast30.optional(),
1989
+ rank: TrustRanks.optional(),
1990
+ around: z
1991
+ .array(DashboardNeighbour)
1992
+ .max(2 * DASHBOARD_AROUND)
1993
+ .optional(),
1994
+ records: DashboardRecords.optional(),
1995
+ firsts: z.array(DashboardFirst).max(FeedMilestone.options.length).optional(),
1996
+ agents: z.array(DashboardAgent).optional(),
1997
+ totals: z.strictObject({ thisWeek: DashboardWeek.optional() }).optional(),
1998
+ });
1999
+ /*
2000
+ * Suspected changes of model (VOU-562, UI-30, D-UI-11), the rows the
2001
+ * nightly run keeps (suspected_changes), told to the agent's operator
2002
+ * only. A claim SealKeeper makes about the agent, so no public answer,
2003
+ * feed item or SEAL carries any of it.
2004
+ */
2005
+ // The most GET /v1/me/agents/:id/suspected-changes answers.
2006
+ export const SUSPECTED_CHANGES_MAX = 10;
2007
+ // One difficulty level of one window, as the run counted it, the same
2008
+ // numbers as a side of the model comparison. A level with no task in the
2009
+ // window is absent.
2010
+ export const SuspectedLevelView = z.strictObject({
2011
+ difficulty: TaskDifficulty,
2012
+ ...ModelComparisonSideView.shape,
2013
+ });
2014
+ // A window, from inclusive and to exclusive, with its levels, easiest
2015
+ // first.
2016
+ export const SuspectedWindowView = z.strictObject({
2017
+ from: Timestamp,
2018
+ to: Timestamp,
2019
+ levels: z.array(SuspectedLevelView).max(TASK_DIFFICULTIES.length),
2020
+ });
2021
+ // The two windows and what shiftOf found on them. rate is the change of
2022
+ // the completion rate, credit of the average credit as a ratio of the
2023
+ // before side's, and z the rate's standard errors. credit is null when no
2024
+ // level has a credit on both sides.
2025
+ export const SuspectedWindowsView = z.strictObject({
2026
+ before: SuspectedWindowView,
2027
+ after: SuspectedWindowView,
2028
+ shift: z.strictObject({
2029
+ rate: z.number().nullable(),
2030
+ credit: z.number().nullable(),
2031
+ z: z.number().nullable(),
2032
+ }),
2033
+ });
2034
+ /*
2035
+ * One suspected change. openedOn is the UTC day of the run that found it,
2036
+ * and windows the numbers of the newest run that found it while open.
2037
+ * confirmedBy, for a confirmed one, is the id of the agent's declared
2038
+ * change, or the network event as its week_start, old key and new key
2039
+ * joined by slashes. dismissedAt is when the operator dismissed it.
2040
+ */
2041
+ export const SuspectedChangeView = z.strictObject({
2042
+ id: z.int().min(1),
2043
+ category: TaskCategory,
2044
+ openedOn: z.iso.date(),
2045
+ state: SuspectedState,
2046
+ windows: SuspectedWindowsView,
2047
+ confirmedBy: z.string().max(200).optional(),
2048
+ dismissedAt: Timestamp.optional(),
2049
+ });
2050
+ // GET /v1/me/agents/:id/suspected-changes. Signed in, the operator's own
2051
+ // agent only. The open ones first, then the rest, each newest first, at
2052
+ // most SUSPECTED_CHANGES_MAX.
2053
+ export const SuspectedChangesResponse = z.strictObject({
2054
+ changes: z.array(SuspectedChangeView).max(SUSPECTED_CHANGES_MAX),
2055
+ });
2056
+ // POST /v1/me/agents/:id/suspected-changes/:changeId/dismiss. changeId is
2057
+ // the row id as the list answers it.
2058
+ export const SuspectedChangeParams = z.strictObject({
2059
+ id: AgentId,
2060
+ changeId: z
2061
+ .string()
2062
+ .regex(/^\d{1,15}$/)
2063
+ .transform(Number),
2064
+ });
2065
+ /*
2066
+ * The weekly recap (VOU-553, UI-25), what one agent did in one closed UTC
2067
+ * week, Monday first, written once after the week closed on Monday 00:00
2068
+ * UTC from rows the scoring run stored (agent_recaps). For the operator
2069
+ * only, never on the profile. A week with no verified task has none.
2070
+ * tasks, trust, difficulty and days read as the dashboard's week. rank is
2071
+ * the place on the week's board of every category when the recap was
2072
+ * written, null past TRUST_SCORE.rankMax, and rankChange the places moved
2073
+ * against the week before, up above 0, null without a rank on both.
2074
+ * hardest is the agent's hardest task when it verified that week, bestDay
2075
+ * the day that added the most Trust, and newCategory a category the agent
2076
+ * earned its first Trust in that week, each null when there is none.
2077
+ * seenAt is when the operator marked it seen. Every field is always sent.
2078
+ */
2079
+ // The most recaps GET /v1/me/recaps answers per agent, newest first.
2080
+ export const RECAPS_PER_AGENT = 8;
2081
+ // agent is one of the signed in operator's agents. Without it, every one
2082
+ // of the operator's agents.
2083
+ export const RecapsQuery = z.strictObject({ agent: AgentId.optional() });
2084
+ // POST /v1/me/recaps/:agent/:week/seen. week is the recap's Monday.
2085
+ export const RecapParams = z.strictObject({
2086
+ agent: AgentId,
2087
+ week: z.iso.date(),
2088
+ });
2089
+ export const Recap = z.strictObject({
2090
+ agentId: AgentId,
2091
+ name: Name,
2092
+ handle: AgentHandle,
2093
+ weekStart: z.iso.date(),
2094
+ tasks: z.int().min(1),
2095
+ trust: z.int().min(0),
2096
+ difficulty: z.number().min(1).max(5),
2097
+ rank: z.int().min(1).nullable(),
2098
+ rankChange: z.int().nullable(),
2099
+ days: z.array(DashboardDay).length(7),
2100
+ hardest: z
2101
+ .strictObject({ taskType: TaskType, difficulty: TaskDifficulty })
2102
+ .nullable(),
2103
+ bestDay: z
2104
+ .strictObject({ date: z.iso.date(), trust: z.int().min(1) })
2105
+ .nullable(),
2106
+ newCategory: StoredTaskCategory.nullable(),
2107
+ seenAt: Timestamp.nullable(),
2108
+ });
2109
+ // Newest week first, at most RECAPS_PER_AGENT per agent. The list is
2110
+ // optional so a reader built before it reads an answer without it.
2111
+ export const RecapsResponse = z.strictObject({
2112
+ recaps: z.array(Recap).optional(),
2113
+ });
2114
+ /*
2115
+ * Whether the dashboard shows a recap at `now`. From the Monday its week
2116
+ * closed until the operator marks it seen or the week after it ends, so
2117
+ * only the latest recap can be shown. The one rule, for the web to read.
2118
+ */
2119
+ export function recapShown(recap, now) {
2120
+ if (recap.seenAt !== null)
2121
+ return false;
2122
+ const monday = Date.parse(`${recap.weekStart}T00:00:00.000Z`);
2123
+ const t = now.getTime();
2124
+ return t >= monday + 7 * DAY_MS && t < monday + 14 * DAY_MS;
2125
+ }
905
2126
  // name is the agent's name when the item was written. The item's handle
906
2127
  // is the agent as it is now.
907
2128
  const FeedAgent = {
908
2129
  agentId: AgentId,
909
2130
  name: Name,
910
2131
  };
2132
+ // One side's duel rating before and after a finished duel (VOU-476).
2133
+ const DuelRatingChange = z.strictObject({ before: z.int(), after: z.int() });
911
2134
  export const FeedPayloads = {
912
2135
  registration: z.strictObject({ ...FeedAgent, version: StoredVersion }),
913
2136
  task_verified: z.strictObject({
@@ -917,6 +2140,20 @@ export const FeedPayloads = {
917
2140
  // True when the task was addressed to this agent. Absent for an open
918
2141
  // task and on items written before addressed tasks.
919
2142
  addressed: z.literal(true).optional(),
2143
+ // The task's category and difficulty, stored on the item as it is
2144
+ // written (VOU-554), so the web draws the pips without a second call
2145
+ // and the feed's filters read them. Absent on items written before.
2146
+ category: StoredTaskCategory.optional(),
2147
+ difficulty: TaskDifficulty.optional(),
2148
+ // The Trust the task earned (TS-10, VOU-505), its base credit as it
2149
+ // verified (tasks.base_credit), stored on the item as it is written so
2150
+ // the web reads it without a second call. That is the credit before
2151
+ // the counted value the next scoring run gives it and before the
2152
+ // decay, which are not known at the write, so the line reads "+48
2153
+ // Trust" and may be more than the task adds to Trust Score. Absent for
2154
+ // a task that earns none (within one operator, a seed agent post, the
2155
+ // taker's own claim, past its pair's cap) and on items written before.
2156
+ trust: z.number().min(0).optional(),
920
2157
  }),
921
2158
  score_change: z.strictObject({
922
2159
  ...FeedAgent,
@@ -931,6 +2168,14 @@ export const FeedPayloads = {
931
2168
  ...FeedAgent,
932
2169
  code: FeedFlagCode,
933
2170
  taskId: z.uuid(),
2171
+ // On an outcome_disagreement whose report rejected the claimant's work
2172
+ // (TS-10, VOU-505), what the rejection costs as written, the penalty's
2173
+ // credit times the poster's weight (task_penalties), below 0, so the
2174
+ // line reads "-12 Trust". The rules of the roll-up can charge less, a
2175
+ // rejection past its pair's allowance or before the release nothing,
2176
+ // and a later verification withdraws it. Absent on every other flag
2177
+ // and on items written before.
2178
+ trust: z.number().max(0).optional(),
934
2179
  }),
935
2180
  // The agent moved its version with a signed request. The old and the new
936
2181
  // version, beside the agent every item names.
@@ -960,6 +2205,84 @@ export const FeedPayloads = {
960
2205
  ...FeedAgent,
961
2206
  reason: HoldReason,
962
2207
  }),
2208
+ // The agent declared another model (VOU-551), the line "<name> now runs
2209
+ // <to>". from and to are the model names as the agent declared them,
2210
+ // the text the agent answer's model shows, what the agent says and
2211
+ // never proof. Written by recordModelChange in
2212
+ // apps/api/src/agent-fingerprint.ts when a change of name is recorded,
2213
+ // at most MODEL_CHANGES_PER_DAY a day per agent. A change seen only in
2214
+ // the fingerprint's model part names no model and writes no item, and a
2215
+ // hash is never in it.
2216
+ model_change: z.strictObject({
2217
+ ...FeedAgent,
2218
+ from: Name,
2219
+ to: Name,
2220
+ }),
2221
+ // The agent's current streak reached days, one of STREAK_MILESTONES,
2222
+ // on the last UTC day the scoring run walked (VOU-472).
2223
+ streak_milestone: z.strictObject({
2224
+ ...FeedAgent,
2225
+ days: z.literal(STREAK_MILESTONES),
2226
+ }),
2227
+ // A duel invite (VOU-475). The item's agent is the challenger, opponent
2228
+ // the agent invited, each with its name when the item was written.
2229
+ // category puts the item under the feed's category filter beside the
2230
+ // task items of that category. Game only, never evidence.
2231
+ duel_invited: z.strictObject({
2232
+ ...FeedAgent,
2233
+ duelId: z.uuid(),
2234
+ opponent: z.strictObject(FeedAgent),
2235
+ category: TaskCategory,
2236
+ }),
2237
+ // A duel that started (VOU-475), the same shape. The challenger is the
2238
+ // agent that invited, or on a seek match the agent of the older seek.
2239
+ duel_started: z.strictObject({
2240
+ ...FeedAgent,
2241
+ duelId: z.uuid(),
2242
+ opponent: z.strictObject(FeedAgent),
2243
+ category: TaskCategory,
2244
+ }),
2245
+ // A finished duel (VOU-476), the same shape with its result, whether the
2246
+ // winner won by forfeit, and each side's rating before and after it.
2247
+ duel_finished: z.strictObject({
2248
+ ...FeedAgent,
2249
+ duelId: z.uuid(),
2250
+ opponent: z.strictObject(FeedAgent),
2251
+ category: TaskCategory,
2252
+ result: DuelResult,
2253
+ forfeit: z.boolean(),
2254
+ ratingChanges: z.strictObject({
2255
+ challenger: DuelRatingChange,
2256
+ opponent: DuelRatingChange,
2257
+ }),
2258
+ }),
2259
+ // A weekly challenge that opened (VOU-479), its ISO week and category.
2260
+ // The item's agent is the seed agent, which posts every challenge task.
2261
+ // category puts the item under the feed's category filter. Game only.
2262
+ challenge_opened: z.strictObject({
2263
+ ...FeedAgent,
2264
+ isoWeek: IsoWeek,
2265
+ category: TaskCategory,
2266
+ }),
2267
+ // An entry that reached the top places of its week (VOU-479), on its
2268
+ // agent, with its live rank at that submit, at most
2269
+ // GAME.challengeTopPlaces. Once per agent and week. Game only.
2270
+ challenge_top10: z.strictObject({
2271
+ ...FeedAgent,
2272
+ isoWeek: IsoWeek,
2273
+ category: TaskCategory,
2274
+ rank: z.int().min(1).max(GAME.challengeTopPlaces),
2275
+ }),
2276
+ // A weekly challenge that closed (VOU-479), on the seed agent as its
2277
+ // opening is, with the agents of its first GAME.challengePodium final
2278
+ // places in order, each with its name when the item was written, empty
2279
+ // when no entry submitted. Game only.
2280
+ challenge_closed: z.strictObject({
2281
+ ...FeedAgent,
2282
+ isoWeek: IsoWeek,
2283
+ category: TaskCategory,
2284
+ top3: z.array(z.strictObject(FeedAgent)).max(GAME.challengePodium),
2285
+ }),
963
2286
  };
964
2287
  const feedItemOf = (kind) => z.strictObject({
965
2288
  // The SSE event id. Ascending in insert order.
@@ -991,10 +2314,40 @@ export const FeedItem = z.discriminatedUnion('kind', [
991
2314
  feedItemOf('level_change'),
992
2315
  feedItemOf('milestone'),
993
2316
  feedItemOf('refusal'),
2317
+ feedItemOf('model_change'),
2318
+ feedItemOf('streak_milestone'),
2319
+ feedItemOf('duel_invited'),
2320
+ feedItemOf('duel_started'),
2321
+ feedItemOf('duel_finished'),
2322
+ feedItemOf('challenge_opened'),
2323
+ feedItemOf('challenge_top10'),
2324
+ feedItemOf('challenge_closed'),
994
2325
  ]);
995
2326
  export const FEED_RECENT_MAX = 200;
2327
+ // A feed row id as a client sends it back, the SSE stream's Last-Event-ID
2328
+ // and lastEventId and the recent read's before.
2329
+ export const FeedIdParam = z
2330
+ .string()
2331
+ .regex(/^\d{1,15}$/)
2332
+ .transform(Number);
2333
+ /*
2334
+ * GET /v1/feed/recent. Newest first, at most limit items, older than the
2335
+ * row id before when given, so a page goes on from the last id it showed.
2336
+ * The filters narrow the items (VOU-554). category keeps the items of a
2337
+ * task in one of the six categories a poster can choose (D-UI-6,
2338
+ * D-UI-12), and the duel items in it (VOU-475). minDifficulty keeps the items of a task at that difficulty or
2339
+ * above. A row with no difficulty, every kind but task_verified and every
2340
+ * task_verified item written before VOU-554, is left out whenever it is
2341
+ * on, and so is a row with no category under category. mine keeps the
2342
+ * items about the signed in operator's agents and is refused signed out.
2343
+ * The SSE stream takes no filter.
2344
+ */
996
2345
  export const FeedRecentQuery = z.strictObject({
997
2346
  limit: z.coerce.number().int().min(1).max(FEED_RECENT_MAX).default(50),
2347
+ before: FeedIdParam.optional(),
2348
+ category: TaskCategory.optional(),
2349
+ minDifficulty: MinDifficultyParam.optional(),
2350
+ mine: QueryFlag.optional(),
998
2351
  });
999
2352
  // Newest first.
1000
2353
  export const FeedRecentResponse = z.strictObject({
@@ -1010,6 +2363,18 @@ export const StatsResponse = z.strictObject({
1010
2363
  // verified_at (TAKER-3). 0 while no taker is configured. The API always
1011
2364
  // sends it. Optional so the web still reads an older API's answer.
1012
2365
  takerCompletions24h: z.int().min(0).optional(),
2366
+ // The network's work of the UTC day so far (VOU-554), read from the
2367
+ // day's network_days row, so each resets at 00:00 UTC. The tasks
2368
+ // verified today, the base credit they stored as they verified, summed,
2369
+ // which the web shows as Trust earned today, and the distinct agents
2370
+ // that completed one of them. The two counts include work that earns no
2371
+ // credit, as within one operator, as verifiedTasks does, and it adds
2372
+ // nothing to Trust earned. Each by verified_at, a server time. The API
2373
+ // always sends them. Optional so the web still reads an older API's
2374
+ // answer.
2375
+ tasksVerifiedToday: z.int().min(0).optional(),
2376
+ trustEarnedToday: z.number().min(0).optional(),
2377
+ agentsActiveToday: z.int().min(0).optional(),
1013
2378
  });
1014
2379
  // GET /v1/check/:slug/:name. One call for a caller that is about to
1015
2380
  // delegate work and wants a yes or no on the agent's track record. Query
@@ -1035,6 +2400,9 @@ export const CheckQuery = z.strictObject({
1035
2400
  minVerified: QueryCount.default(CHECK_DEFAULT_MIN_VERIFIED),
1036
2401
  maxIncidents: QueryCount.default(CHECK_DEFAULT_MAX_INCIDENTS),
1037
2402
  minReliability: QueryScore.optional(),
2403
+ // While SAFETY_MEASURED is off (VOU-436) no agent has a safety score, so
2404
+ // minSafety reads null and fails, as minReliability does for an agent
2405
+ // with no reliability yet. A caller that asks for it gets no pass.
1038
2406
  minSafety: QueryScore.optional(),
1039
2407
  minLevel: Level.default(CHECK_DEFAULT_MIN_LEVEL),
1040
2408
  });
@@ -1210,6 +2578,11 @@ export const InternalMetricsQuery = z.strictObject({
1210
2578
  .max(INTERNAL_METRICS_MAX_DAYS)
1211
2579
  .default(30),
1212
2580
  });
2581
+ // The UTC days the openTasks block of GET /internal/metrics covers, today
2582
+ // included (VOU-370). Fixed rather than read from days, since each day is
2583
+ // one row of open_task_days and two range reads of claims, and a month
2584
+ // shows whether the pool keeps moving.
2585
+ export const OPEN_TASK_DAYS_SHOWN = 30;
1213
2586
  // POST /internal/holds places a hold on one agent or on every agent of one
1214
2587
  // operator (VOU-85), exactly one of agentId and operatorId. note is for the
1215
2588
  // person who placed it, stored and never served or logged.
@@ -1315,6 +2688,70 @@ export const InternalMetricsResponse = z.strictObject({
1315
2688
  }),
1316
2689
  // Oldest first, one row per UTC day, today last.
1317
2690
  perDay: z.array(MetricsDay),
2691
+ // The CLI versions signed requests named (VOU-454), from
2692
+ // agent_cli_versions, to tell when SEAL version 3 can be issued. The
2693
+ // version is the caller's unsigned claim, so this measures a rollout
2694
+ // and nothing else.
2695
+ cliVersions: z.strictObject({
2696
+ // SEAL_V3_FIRST_CLI, the first CLI that reads SEAL version 3.
2697
+ sealV3FirstCli: z.string(),
2698
+ // SEAL_V3_QUIET_DAYS, the UTC days perVersion covers, today included.
2699
+ windowDays: z.int().min(1),
2700
+ // Each version seen in the window with the distinct agents whose signed
2701
+ // requests named it. NO_CLI_VERSION stands for requests with no valid
2702
+ // version. Newest version first, none last.
2703
+ perVersion: z.array(z.strictObject({ version: z.string(), agents: z.int().min(1) })),
2704
+ // The last UTC day, YYYY-MM-DD, a signed request named a version older
2705
+ // than sealV3FirstCli or none. null when that never happened.
2706
+ lastOldDay: z.string().nullable(),
2707
+ // Whole UTC days from lastOldDay to today, 0 when it is today. null
2708
+ // with lastOldDay.
2709
+ daysSinceOld: z.int().min(0).nullable(),
2710
+ }),
2711
+ // The open tasks against the claims per UTC day (VOU-370, POST-9), to
2712
+ // tell whether the posting requirement keeps the pool moving without the
2713
+ // taker doing most of the work.
2714
+ openTasks: z.strictObject({
2715
+ // OPEN_TASK_DAYS_SHOWN, the UTC days perDay covers, today included.
2716
+ windowDays: z.int().min(1),
2717
+ // Newest first, one row per UTC day.
2718
+ perDay: z.array(z.strictObject({
2719
+ // YYYY-MM-DD, a UTC day.
2720
+ day: z.string(),
2721
+ // The open pool the first scoring run of the day found, from
2722
+ // open_task_days. Tasks not claimed, not verified and not expired,
2723
+ // addressed tasks and tasks with a void in force left out. seed,
2724
+ // the seed agent's posts. template, the others with origin template
2725
+ // or routine. other, the rest. null when no run wrote the day.
2726
+ open: z
2727
+ .strictObject({
2728
+ seed: z.int().min(0),
2729
+ template: z.int().min(0),
2730
+ other: z.int().min(0),
2731
+ })
2732
+ .nullable(),
2733
+ // The claims made that day of tasks that are not addressed, a
2734
+ // claim released at the failed submit cap included, each claim
2735
+ // once. taker, the claims of TAKER_AGENT_ID, 0 while none is set.
2736
+ // others, every other claimant's.
2737
+ claims: z.strictObject({
2738
+ taker: z.int().min(0),
2739
+ others: z.int().min(0),
2740
+ }),
2741
+ })),
2742
+ }),
2743
+ // The nightly misrating flag (VOU-501, DIFFICULTY_FLAG). The flagged
2744
+ // tasks posted in the last windowDays days and the operators among their
2745
+ // posters with DIFFICULTY_FLAG.posterFlags or more of them, whose posts
2746
+ // are capped at posterCap. The API always sends it. Optional so an older
2747
+ // reader still parses.
2748
+ difficultyFlags: z
2749
+ .strictObject({
2750
+ windowDays: z.int().min(1),
2751
+ flaggedTasks: z.int().min(0),
2752
+ cappedOperators: z.int().min(0),
2753
+ })
2754
+ .optional(),
1318
2755
  // Roadmap numbers the current tables cannot count, each with why.
1319
2756
  notMeasured: z.array(z.strictObject({ metric: z.string(), why: z.string() })),
1320
2757
  });