plures 1.4.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -26,7 +26,7 @@ __export(index_exports, {
26
26
  default: () => index_default
27
27
  });
28
28
  module.exports = __toCommonJS(index_exports);
29
- var VERSION = "1.4.0";
29
+ var VERSION = "1.7.0";
30
30
  var PluresError = class extends Error {
31
31
  constructor(message, status, type) {
32
32
  super(message);
@@ -39,17 +39,22 @@ var PluresError = class extends Error {
39
39
  var Plures = class {
40
40
  base;
41
41
  apiKey;
42
+ /** The app tasks go to when they don't name one. From opts.app or PLURES_APP. */
43
+ app;
44
+ ua;
42
45
  constructor(apiKey, opts = {}) {
43
46
  const key = apiKey ?? (typeof process !== "undefined" ? process.env?.PLURES_API_KEY : void 0);
44
47
  if (!key) throw new Error("Pass your Plures API key, or set PLURES_API_KEY. Get a free test key at https://plures.si/?for=developers#join");
45
48
  this.apiKey = key;
49
+ this.ua = opts.userAgent ?? `plures-js/${VERSION}`;
50
+ this.app = opts.app ?? (typeof process !== "undefined" ? process.env?.PLURES_APP : void 0) ?? null;
46
51
  this.base = (opts.baseUrl ?? (typeof process !== "undefined" ? process.env?.PLURES_BASE_URL : void 0) ?? "https://plures.si").replace(/\/$/, "");
47
52
  }
48
53
  async req(method, path, body, headers = {}) {
49
54
  for (let attempt = 0; ; attempt++) {
50
55
  const r = await fetch(this.base + path, {
51
56
  method,
52
- headers: { authorization: `Bearer ${this.apiKey}`, "user-agent": `plures-js/${VERSION}`, ...body ? { "content-type": "application/json" } : {}, ...headers },
57
+ headers: { authorization: `Bearer ${this.apiKey}`, "user-agent": this.ua, ...body ? { "content-type": "application/json" } : {}, ...this.app ? { "plures-app": this.app } : {}, ...headers },
53
58
  body: body ? JSON.stringify(body) : void 0
54
59
  });
55
60
  if (r.status >= 500 && attempt < 2) {
@@ -92,6 +97,69 @@ var Plures = class {
92
97
  set: (mix) => this.req("PUT", "/api/v1/test_mix", { mix }),
93
98
  clear: () => this.req("DELETE", "/api/v1/test_mix")
94
99
  };
100
+ /**
101
+ * Where tasks go. set() replaces what you send (team, groups or rules) and keeps the rest.
102
+ * For example { groups: [{ name: "Finance", emails: ["ap@acme.com"] }], rules: [{ category: "refunds", route: "team", group: "Finance" }] }.
103
+ */
104
+ routing = {
105
+ get: () => this.req("GET", "/api/v1/routing"),
106
+ set: (update) => this.req("PUT", "/api/v1/routing", update)
107
+ };
108
+ /** Your apps. Each agent you run can be its own app, with its own tasks, ledger view and map. */
109
+ apps = {
110
+ list: () => this.req("GET", "/api/v1/apps"),
111
+ /** { name: "Support agent" }, with an optional id like "support-agent". */
112
+ create: (params) => this.req("POST", "/api/v1/apps", params),
113
+ retrieve: (id) => this.req("GET", `/api/v1/apps/${encodeURIComponent(id)}`),
114
+ /** Rename it, change its id (its tasks move with it), or make it the default. */
115
+ update: (id, params) => this.req("PATCH", `/api/v1/apps/${encodeURIComponent(id)}`, params),
116
+ /** Only an app with no tasks can be deleted. */
117
+ del: (id) => this.req("DELETE", `/api/v1/apps/${encodeURIComponent(id)}`)
118
+ };
119
+ /**
120
+ * Agent maps: where each app's agent needs a person. Your coding agent reads your code and
121
+ * writes the map; Plures gets only the map. guide() returns the steps it follows.
122
+ */
123
+ maps = {
124
+ guide: async () => {
125
+ const r = await fetch(`${this.base}/api/v1/map_guide`);
126
+ return r.text();
127
+ },
128
+ retrieve: (app) => this.req("GET", `/api/v1/apps/${encodeURIComponent(app)}/map`),
129
+ /** Uploads a new version. Checks you already added or dismissed keep that. */
130
+ upload: (app, map) => this.req("PUT", `/api/v1/apps/${encodeURIComponent(app)}/map`, map),
131
+ setCheckpoint: (app, id, status) => this.req("PATCH", `/api/v1/apps/${encodeURIComponent(app)}/map/checkpoints/${encodeURIComponent(id)}`, { status })
132
+ };
133
+ /**
134
+ * Quality: people grade a sample of your agent's outputs against a rubric, so you see drift
135
+ * early, and compare two versions blind before you ship. Personal details are taken out first.
136
+ */
137
+ quality = {
138
+ /** Send every output; the rubric's sample rate decides which ones people grade. */
139
+ sample: (p) => this.req("POST", "/api/v1/samples", p),
140
+ /** A blind release check: a is the current version's output, b the new one's, for the same input. */
141
+ compare: (p) => this.req("POST", "/api/v1/comparisons", p),
142
+ /** How often people preferred the new version, with a 95% range and a verdict. */
143
+ release: (name, opts = {}) => this.req("GET", `/api/v1/releases/${encodeURIComponent(name)}${opts.app ? `?app=${encodeURIComponent(opts.app)}` : ""}`),
144
+ rubrics: {
145
+ list: (opts = {}) => this.req("GET", `/api/v1/rubrics${opts.app ? `?app=${encodeURIComponent(opts.app)}` : ""}`),
146
+ /** { name: "reply_quality", question: "Is this reply correct and kind?", options: ["Good", "Okay", "Bad"], sample_rate: 0.05 } */
147
+ create: (p) => this.req("POST", "/api/v1/rubrics", p),
148
+ retrieve: (name, opts = {}) => this.req("GET", `/api/v1/rubrics/${encodeURIComponent(name)}${opts.app ? `?app=${encodeURIComponent(opts.app)}` : ""}`),
149
+ update: (name, p) => this.req("PATCH", `/api/v1/rubrics/${encodeURIComponent(name)}${p.app ? `?app=${encodeURIComponent(p.app)}` : ""}`, p),
150
+ del: (name, opts = {}) => this.req("DELETE", `/api/v1/rubrics/${encodeURIComponent(name)}${opts.app ? `?app=${encodeURIComponent(opts.app)}` : ""}`)
151
+ },
152
+ /** Graded samples as JSON lines (input, output, grade, score, agreement), for fine tuning or an eval set. */
153
+ export: async (rubric, opts = {}) => {
154
+ const q = new URLSearchParams(Object.entries({ app: opts.app ?? this.app ?? void 0, since: opts.since }).filter(([, v]) => v));
155
+ const r = await fetch(`${this.base}/api/v1/rubrics/${encodeURIComponent(rubric)}/export${q.size ? `?${q}` : ""}`, { headers: { authorization: `Bearer ${this.apiKey}`, "user-agent": this.ua } });
156
+ if (!r.ok) {
157
+ const d = await r.json().catch(() => ({}));
158
+ throw new PluresError(d?.error?.message ?? `HTTP ${r.status}`, r.status, d?.error?.type ?? "api_error");
159
+ }
160
+ return (await r.text()).split("\n").filter(Boolean).map((l) => JSON.parse(l));
161
+ }
162
+ };
95
163
  /** Your prepaid credit. A live task that could cost more than available_cents is refused with a 402 insufficient_credits error. */
96
164
  balance = () => this.req("GET", "/api/v1/balance");
97
165
  /** Creates a task and waits for people to answer it. */
package/dist/index.d.cts CHANGED
@@ -2,6 +2,8 @@ type Option = {
2
2
  id: string;
3
3
  label: string;
4
4
  };
5
+ /** "verifiers" asks ID verified people first; "team" goes straight to your own people ($0.05 per internal vote live). */
6
+ type Route = "verifiers" | "team";
5
7
  type TaskStatus = "pending" | "completed" | "expired" | "canceled";
6
8
  type Result = {
7
9
  answer: string | null;
@@ -15,17 +17,44 @@ type Result = {
15
17
  /** The team member who decided, when decided_by is "team". */
16
18
  reviewer?: string;
17
19
  note?: string;
20
+ /** How your team voted, when your team decided. */
21
+ team?: TeamTally;
18
22
  };
19
- /** Set when verifiers didn't all agree and the decision went to your team. */
23
+ type TeamVote = {
24
+ reviewer: string;
25
+ answer: string;
26
+ note: string | null;
27
+ confirmed: boolean;
28
+ lead: boolean;
29
+ };
30
+ type TeamTally = {
31
+ votes: Record<string, number>;
32
+ agreement: number;
33
+ required: number;
34
+ unanimous: boolean;
35
+ voters: TeamVote[];
36
+ decided_by_lead: boolean;
37
+ agreed: boolean;
38
+ };
39
+ /** How your team decides: how many vote, whether they must all agree, a lead for splits, passkey confirmation. */
40
+ type Consensus = {
41
+ votes: number;
42
+ unanimous: boolean;
43
+ lead: string | null;
44
+ passkey: boolean;
45
+ };
46
+ /** Set when the decision went to your team: straight there (team_only), or because verifiers didn't all agree or answer in time. */
20
47
  type Escalation = {
21
- reason: "split" | "tie" | "no_answer";
48
+ reason: "split" | "tie" | "no_answer" | "team_only";
22
49
  escalated_at: string;
23
50
  verifiers: Result | null;
24
51
  team_deadline?: string;
52
+ lead_called_at?: string;
25
53
  };
26
54
  type Task = {
27
55
  id: string;
28
56
  object: "task";
57
+ app?: string | null;
29
58
  mode: "live" | "test";
30
59
  status: TaskStatus;
31
60
  question: string;
@@ -42,11 +71,23 @@ type Task = {
42
71
  result: Result | null;
43
72
  escalation?: Escalation | null;
44
73
  metadata: Record<string, unknown>;
45
- /** Live tasks only: price per answer and what answers have cost so far, in US cents. */
74
+ /** Where it went: verified people first, or straight to your team. */
75
+ route?: Route;
76
+ category?: string | null;
77
+ /** The named team that decides, or null for your main team. */
78
+ team?: string | null;
79
+ team_consensus?: {
80
+ votes: number;
81
+ unanimous: boolean;
82
+ lead: boolean;
83
+ passkey: boolean;
84
+ };
85
+ /** Live tasks only: price per answer and what answers have cost so far, in US cents. A task sent straight to your team is billed per internal vote, billed_as "internal_votes". */
46
86
  cost?: {
47
87
  per_answer_cents: number;
48
88
  answers_billed: number;
49
89
  total_cents: number;
90
+ billed_as?: "answers" | "internal_votes";
50
91
  } | null;
51
92
  /** Where this decision sits on the Plures ledger, once recorded. */
52
93
  ledger?: {
@@ -83,10 +124,59 @@ type CreateTask = {
83
124
  escalate?: boolean;
84
125
  /** Agreement below which this task goes to your team, 0 to 1. Default from your dashboard. */
85
126
  escalate_below?: number;
127
+ /** "team" skips verified people and sends it straight to your team. Default "verifiers". */
128
+ route?: Route;
129
+ /** A label your routing rules match on, like "refunds". */
130
+ category?: string;
131
+ /** The name of one of your teams, like "Finance", instead of your main team. */
132
+ team?: string;
133
+ /** How many people on the team vote; the majority decides. Default from the team. */
134
+ team_votes?: number;
135
+ /** Everyone who votes must agree, or it goes to the team's lead. */
136
+ team_unanimous?: boolean;
137
+ /** Which of your apps this task belongs to, like "support-agent". Defaults to the client's app, then your default app. */
138
+ app?: string;
139
+ };
140
+ /** Your main team, named teams and rules by category. */
141
+ type RoutingRule = {
142
+ category: string;
143
+ route: Route;
144
+ group: string | null;
145
+ escalate_below: number | null;
146
+ };
147
+ type Routing = {
148
+ object: "routing";
149
+ team: {
150
+ emails: string[];
151
+ escalate_below: number;
152
+ } & Consensus;
153
+ groups: ({
154
+ name: string;
155
+ emails: string[];
156
+ } & Consensus)[];
157
+ rules: RoutingRule[];
158
+ internal_vote_cents: number;
159
+ };
160
+ type RoutingUpdate = {
161
+ team?: {
162
+ emails?: string[];
163
+ escalate_below?: number;
164
+ } & Partial<Consensus>;
165
+ groups?: ({
166
+ name: string;
167
+ emails: string[];
168
+ } & Partial<Consensus>)[];
169
+ rules?: {
170
+ category: string;
171
+ route?: Route;
172
+ group?: string | null;
173
+ escalate_below?: number | null;
174
+ }[];
86
175
  };
87
176
  /** Whole number shares of test task outcomes that add up to 100. */
88
177
  type Mix = {
89
178
  unanimous?: number;
179
+ most?: number;
90
180
  split?: number;
91
181
  tie?: number;
92
182
  expired?: number;
@@ -97,6 +187,140 @@ type TestMix = {
97
187
  to_team_percent: number;
98
188
  };
99
189
  /** Prepaid credit for live tasks, in US cents. */
190
+ type MapNode = {
191
+ id: string;
192
+ label: string;
193
+ kind: "input" | "model" | "tool" | "action" | "data" | "output" | "human";
194
+ file?: string | null;
195
+ line?: number | null;
196
+ risk?: "low" | "medium" | "high" | null;
197
+ note?: string | null;
198
+ };
199
+ type MapDraft = {
200
+ question: string;
201
+ options: string[];
202
+ show?: string[];
203
+ hide?: string[];
204
+ instructions?: string | null;
205
+ route?: "verifiers" | "team";
206
+ team?: string | null;
207
+ verifiers?: number;
208
+ escalate_below?: number | null;
209
+ example?: string | null;
210
+ };
211
+ type MapCheckpoint = {
212
+ id?: string;
213
+ at: string;
214
+ why: string;
215
+ category?: string;
216
+ when?: string | null;
217
+ status?: "suggested" | "added" | "dismissed";
218
+ draft?: MapDraft | null;
219
+ stats?: {
220
+ tasks: number;
221
+ done: number;
222
+ agreement: number | null;
223
+ to_team: number;
224
+ internal: number;
225
+ med_ms: number | null;
226
+ };
227
+ };
228
+ /** An agent map: the steps of one app's agent and where people check. Written by your coding agent; no source code. */
229
+ type AgentMap = {
230
+ name?: string | null;
231
+ summary?: string | null;
232
+ source?: {
233
+ repo?: string | null;
234
+ commit?: string | null;
235
+ tool?: string | null;
236
+ };
237
+ nodes: MapNode[];
238
+ edges: {
239
+ from: string;
240
+ to: string;
241
+ label?: string | null;
242
+ }[];
243
+ checkpoints: MapCheckpoint[];
244
+ };
245
+ type StoredAgentMap = AgentMap & {
246
+ object: "agent_map";
247
+ app: string;
248
+ version: number;
249
+ created_at?: string;
250
+ via?: string;
251
+ url?: string;
252
+ };
253
+ type Rubric = {
254
+ object: "rubric";
255
+ name: string;
256
+ app: string;
257
+ question: string;
258
+ options: string[];
259
+ instructions: string | null;
260
+ sample_rate: number;
261
+ verifiers: number;
262
+ drift_points: number;
263
+ webhook_url: string | null;
264
+ seen: number;
265
+ sampled: number;
266
+ created_at: string;
267
+ quality?: {
268
+ score: number | null;
269
+ this_week: number | null;
270
+ baseline: number | null;
271
+ drift: boolean;
272
+ graded_this_week: number;
273
+ grades_this_week: Record<string, number>;
274
+ days: {
275
+ day: string;
276
+ score: number | null;
277
+ n: number;
278
+ }[];
279
+ };
280
+ };
281
+ type RubricParams = {
282
+ name?: string;
283
+ question?: string;
284
+ options?: string[];
285
+ instructions?: string | null;
286
+ sample_rate?: number;
287
+ verifiers?: 1 | 3 | 5;
288
+ drift_points?: number;
289
+ webhook_url?: string | null;
290
+ app?: string;
291
+ };
292
+ type Sample = {
293
+ object: "sample";
294
+ rubric: string;
295
+ sampled: boolean;
296
+ sample_rate: number;
297
+ redacted: Record<string, number>;
298
+ task: Task | null;
299
+ };
300
+ type Release = {
301
+ object: "release";
302
+ release: string;
303
+ app: string;
304
+ compared: number;
305
+ b_wins: number;
306
+ a_wins: number;
307
+ ties: number;
308
+ win_rate: number | null;
309
+ low: number | null;
310
+ high: number | null;
311
+ verdict: string;
312
+ };
313
+ /** One agent or product on your account. Tasks, the ledger view and the map are kept per app. */
314
+ type App = {
315
+ object: "app";
316
+ id: string;
317
+ name: string;
318
+ default: boolean;
319
+ created_at: string;
320
+ tasks_30d?: number;
321
+ waiting?: number;
322
+ last_task_at?: string | null;
323
+ };
100
324
  type Balance = {
101
325
  object: "balance";
102
326
  currency: "usd";
@@ -117,7 +341,7 @@ type WebhookEvent = {
117
341
  created_at: string;
118
342
  data: Task;
119
343
  };
120
- declare const VERSION = "1.4.0";
344
+ declare const VERSION = "1.7.0";
121
345
  declare class PluresError extends Error {
122
346
  readonly status: number;
123
347
  readonly type: string;
@@ -126,8 +350,13 @@ declare class PluresError extends Error {
126
350
  declare class Plures {
127
351
  private base;
128
352
  private apiKey;
353
+ /** The app tasks go to when they don't name one. From opts.app or PLURES_APP. */
354
+ readonly app: string | null;
355
+ private ua;
129
356
  constructor(apiKey?: string, opts?: {
130
357
  baseUrl?: string;
358
+ app?: string;
359
+ userAgent?: string;
131
360
  });
132
361
  private req;
133
362
  tasks: {
@@ -140,6 +369,7 @@ declare class Plures {
140
369
  status?: TaskStatus;
141
370
  limit?: number;
142
371
  starting_after?: string;
372
+ app?: string;
143
373
  }) => Promise<{
144
374
  object: "list";
145
375
  data: Task[];
@@ -163,6 +393,126 @@ declare class Plures {
163
393
  set: (mix: Mix | null) => Promise<TestMix>;
164
394
  clear: () => Promise<TestMix>;
165
395
  };
396
+ /**
397
+ * Where tasks go. set() replaces what you send (team, groups or rules) and keeps the rest.
398
+ * For example { groups: [{ name: "Finance", emails: ["ap@acme.com"] }], rules: [{ category: "refunds", route: "team", group: "Finance" }] }.
399
+ */
400
+ routing: {
401
+ get: () => Promise<Routing>;
402
+ set: (update: RoutingUpdate) => Promise<Routing>;
403
+ };
404
+ /** Your apps. Each agent you run can be its own app, with its own tasks, ledger view and map. */
405
+ apps: {
406
+ list: () => Promise<{
407
+ object: "list";
408
+ data: App[];
409
+ has_more: boolean;
410
+ }>;
411
+ /** { name: "Support agent" }, with an optional id like "support-agent". */
412
+ create: (params: {
413
+ name: string;
414
+ id?: string;
415
+ }) => Promise<App>;
416
+ retrieve: (id: string) => Promise<App>;
417
+ /** Rename it, change its id (its tasks move with it), or make it the default. */
418
+ update: (id: string, params: {
419
+ name?: string;
420
+ id?: string;
421
+ default?: true;
422
+ }) => Promise<App>;
423
+ /** Only an app with no tasks can be deleted. */
424
+ del: (id: string) => Promise<{
425
+ object: "app";
426
+ id: string;
427
+ deleted: true;
428
+ }>;
429
+ };
430
+ /**
431
+ * Agent maps: where each app's agent needs a person. Your coding agent reads your code and
432
+ * writes the map; Plures gets only the map. guide() returns the steps it follows.
433
+ */
434
+ maps: {
435
+ guide: () => Promise<string>;
436
+ retrieve: (app: string) => Promise<StoredAgentMap>;
437
+ /** Uploads a new version. Checks you already added or dismissed keep that. */
438
+ upload: (app: string, map: AgentMap) => Promise<StoredAgentMap>;
439
+ setCheckpoint: (app: string, id: string, status: "added" | "dismissed" | "suggested") => Promise<StoredAgentMap>;
440
+ };
441
+ /**
442
+ * Quality: people grade a sample of your agent's outputs against a rubric, so you see drift
443
+ * early, and compare two versions blind before you ship. Personal details are taken out first.
444
+ */
445
+ quality: {
446
+ /** Send every output; the rubric's sample rate decides which ones people grade. */
447
+ sample: (p: {
448
+ rubric: string;
449
+ output: string;
450
+ input?: string;
451
+ app?: string;
452
+ metadata?: Record<string, unknown>;
453
+ force?: boolean;
454
+ }) => Promise<Sample>;
455
+ /** A blind release check: a is the current version's output, b the new one's, for the same input. */
456
+ compare: (p: {
457
+ release: string;
458
+ a: string;
459
+ b: string;
460
+ input?: string;
461
+ question?: string;
462
+ instructions?: string;
463
+ verifiers?: 1 | 3 | 5;
464
+ app?: string;
465
+ metadata?: Record<string, unknown>;
466
+ }) => Promise<{
467
+ object: "comparison";
468
+ release: string;
469
+ task: Task;
470
+ }>;
471
+ /** How often people preferred the new version, with a 95% range and a verdict. */
472
+ release: (name: string, opts?: {
473
+ app?: string;
474
+ }) => Promise<Release>;
475
+ rubrics: {
476
+ list: (opts?: {
477
+ app?: string;
478
+ }) => Promise<{
479
+ object: "list";
480
+ data: Rubric[];
481
+ has_more: boolean;
482
+ }>;
483
+ /** { name: "reply_quality", question: "Is this reply correct and kind?", options: ["Good", "Okay", "Bad"], sample_rate: 0.05 } */
484
+ create: (p: RubricParams & {
485
+ name: string;
486
+ question: string;
487
+ options: string[];
488
+ }) => Promise<Rubric>;
489
+ retrieve: (name: string, opts?: {
490
+ app?: string;
491
+ }) => Promise<Rubric>;
492
+ update: (name: string, p: RubricParams) => Promise<Rubric>;
493
+ del: (name: string, opts?: {
494
+ app?: string;
495
+ }) => Promise<{
496
+ deleted: true;
497
+ }>;
498
+ };
499
+ /** Graded samples as JSON lines (input, output, grade, score, agreement), for fine tuning or an eval set. */
500
+ export: (rubric: string, opts?: {
501
+ app?: string;
502
+ since?: string;
503
+ }) => Promise<{
504
+ task_id: string;
505
+ rubric: string;
506
+ input: string | null;
507
+ output: string;
508
+ grade: string | null;
509
+ score: number | null;
510
+ agreement: number | null;
511
+ graders: number;
512
+ graded_at: string;
513
+ metadata: Record<string, unknown>;
514
+ }[]>;
515
+ };
166
516
  /** Your prepaid credit. A live task that could cost more than available_cents is refused with a 402 insufficient_credits error. */
167
517
  balance: () => Promise<Balance>;
168
518
  /** Creates a task and waits for people to answer it. */
@@ -178,4 +528,4 @@ declare class Plures {
178
528
  static verifyWebhook(rawBody: string, signatureHeader: string | null, secret: string, toleranceSec?: number): Promise<WebhookEvent>;
179
529
  }
180
530
 
181
- export { type Balance, type CreateTask, type Escalation, type Mix, type Option, Plures, PluresError, type Result, type Task, type TaskStatus, type TestMix, VERSION, type WebhookEvent, Plures as default };
531
+ export { type AgentMap, type App, type Balance, type Consensus, type CreateTask, type Escalation, type MapCheckpoint, type MapDraft, type MapNode, type Mix, type Option, Plures, PluresError, type Release, type Result, type Route, type Routing, type RoutingRule, type RoutingUpdate, type Rubric, type RubricParams, type Sample, type StoredAgentMap, type Task, type TaskStatus, type TeamTally, type TeamVote, type TestMix, VERSION, type WebhookEvent, Plures as default };