@herbertgao/pi-extensions 2026.9.8 → 2026.9.10
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/README.md +8 -4
- package/THIRD_PARTY_NOTICES.md +52 -0
- package/node_modules/@herbertgao/pi-bark/package.json +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +4 -4
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +6 -0
- package/node_modules/@herbertgao/pi-subagents/README.md +427 -120
- package/node_modules/@herbertgao/pi-subagents/docs/rpc.md +184 -0
- package/node_modules/@herbertgao/pi-subagents/docs/workflows.md +466 -0
- package/node_modules/@herbertgao/pi-subagents/examples/agent-tool-description.md +6 -6
- package/node_modules/@herbertgao/pi-subagents/examples/workflows/compose.js +52 -0
- package/node_modules/@herbertgao/pi-subagents/examples/workflows/fan-out-audit.js +56 -0
- package/node_modules/@herbertgao/pi-subagents/examples/workflows/gated-fix.js +60 -0
- package/node_modules/@herbertgao/pi-subagents/examples/workflows/lib/count-child.js +30 -0
- package/node_modules/@herbertgao/pi-subagents/examples/workflows/review-panel.js +68 -0
- package/node_modules/@herbertgao/pi-subagents/examples/workflows/structured-findings.js +81 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +11 -9
- package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +52 -12
- package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +837 -146
- package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +213 -39
- package/node_modules/@herbertgao/pi-subagents/src/cross-extension-rpc.ts +73 -14
- package/node_modules/@herbertgao/pi-subagents/src/custom-agents.ts +101 -47
- package/node_modules/@herbertgao/pi-subagents/src/index.ts +2249 -914
- package/node_modules/@herbertgao/pi-subagents/src/invocation-config.ts +13 -0
- package/node_modules/@herbertgao/pi-subagents/src/mention-clone.ts +215 -0
- package/node_modules/@herbertgao/pi-subagents/src/mention.ts +147 -0
- package/node_modules/@herbertgao/pi-subagents/src/model-resolver.ts +9 -1
- package/node_modules/@herbertgao/pi-subagents/src/nested-tools.ts +40 -26
- package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +18 -8
- package/node_modules/@herbertgao/pi-subagents/src/prompts.ts +46 -9
- package/node_modules/@herbertgao/pi-subagents/src/schedule.ts +21 -16
- package/node_modules/@herbertgao/pi-subagents/src/settings.ts +137 -7
- package/node_modules/@herbertgao/pi-subagents/src/structured-output.ts +136 -0
- package/node_modules/@herbertgao/pi-subagents/src/types.ts +126 -8
- package/node_modules/@herbertgao/pi-subagents/src/ui/agent-mention.ts +274 -0
- package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +20 -5
- package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +10 -4
- package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +167 -22
- package/node_modules/@herbertgao/pi-subagents/src/ui/workflow-card.ts +555 -0
- package/node_modules/@herbertgao/pi-subagents/src/ui/workflow-dialog.ts +1304 -0
- package/node_modules/@herbertgao/pi-subagents/src/ui/workflow-menu.ts +226 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/collisions.ts +122 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/entry.ts +47 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/host.ts +463 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/journal.ts +164 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/json-schema.ts +142 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/meta.ts +401 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/progress.ts +622 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/runtime.ts +1399 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/saved.ts +230 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/task.ts +333 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/tool-description.ts +200 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/worker-source.ts +781 -0
- package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +97 -95
- package/node_modules/@herbertgao/pi-subagents/src/xml.ts +13 -0
- package/node_modules/@herbertgao/resume-from/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/README.md +6 -1
- package/node_modules/@narumitw/pi-btw/dist/index.ts +209 -23
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +4 -4
- package/node_modules/@narumitw/pi-btw/package.json +1 -1
- package/node_modules/@narumitw/pi-btw/src/btw.ts +1 -12
- package/node_modules/@narumitw/pi-btw/src/menu.ts +244 -15
- package/node_modules/@narumitw/pi-btw/src/settings.ts +5 -0
- package/node_modules/pi-jev-auto-mode/CHANGELOG.md +110 -0
- package/node_modules/pi-jev-auto-mode/LICENSE +21 -0
- package/node_modules/pi-jev-auto-mode/README.md +285 -0
- package/node_modules/pi-jev-auto-mode/SECURITY.md +32 -0
- package/node_modules/pi-jev-auto-mode/docs/calibration.md +154 -0
- package/node_modules/pi-jev-auto-mode/docs/design.md +164 -0
- package/node_modules/pi-jev-auto-mode/docs/security.md +124 -0
- package/node_modules/pi-jev-auto-mode/index.ts +1 -0
- package/node_modules/pi-jev-auto-mode/package.json +72 -0
- package/node_modules/pi-jev-auto-mode/src/call.ts +180 -0
- package/node_modules/pi-jev-auto-mode/src/decide.ts +86 -0
- package/node_modules/pi-jev-auto-mode/src/extension.ts +862 -0
- package/node_modules/pi-jev-auto-mode/src/intent.ts +71 -0
- package/node_modules/pi-jev-auto-mode/src/jev/availability.ts +53 -0
- package/node_modules/pi-jev-auto-mode/src/jev/criteria.ts +19 -0
- package/node_modules/pi-jev-auto-mode/src/jev/decide.ts +187 -0
- package/node_modules/pi-jev-auto-mode/src/jev/engine.ts +165 -0
- package/node_modules/pi-jev-auto-mode/src/jev/index.ts +20 -0
- package/node_modules/pi-jev-auto-mode/src/jev/questions.ts +273 -0
- package/node_modules/pi-jev-auto-mode/src/jev/response.ts +64 -0
- package/node_modules/pi-jev-auto-mode/src/jev/state.ts +20 -0
- package/node_modules/pi-jev-auto-mode/src/jev/transport.ts +117 -0
- package/node_modules/pi-jev-auto-mode/src/jev/types.ts +46 -0
- package/node_modules/pi-jev-auto-mode/src/policy.ts +558 -0
- package/node_modules/pi-jev-auto-mode/src/records.ts +118 -0
- package/node_modules/pi-jev-auto-mode/src/settings.ts +327 -0
- package/node_modules/pi-jev-auto-mode/src/ui.ts +233 -0
- package/node_modules/pi-lens/CHANGELOG.md +45 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +21 -9
- package/node_modules/pi-lens/dist/clients/cascade-format.js +24 -0
- package/node_modules/pi-lens/dist/clients/extension-log.js +48 -1
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +11 -12
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +3 -0
- package/node_modules/pi-lens/dist/clients/lsp/index.js +41 -7
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +9 -2
- package/node_modules/pi-lens/dist/clients/path-utils.js +51 -9
- package/node_modules/pi-lens/dist/clients/persistent-reverify.js +271 -0
- package/node_modules/pi-lens/dist/clients/pipeline.js +15 -1
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +124 -7
- package/node_modules/pi-lens/dist/clients/skills-resolver.js +38 -2
- package/node_modules/pi-lens/dist/clients/widget-state.js +152 -6
- package/node_modules/pi-lens/dist/index.js +729 -344
- package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +14 -2
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +84 -17
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +32 -5
- package/node_modules/pi-lens/package.json +1 -1
- package/node_modules/pi-typesafe/LICENSE +21 -0
- package/node_modules/pi-typesafe/README.md +153 -0
- package/node_modules/pi-typesafe/dist/ask.d.ts +32 -0
- package/node_modules/pi-typesafe/dist/ask.js +25 -0
- package/node_modules/pi-typesafe/dist/auth.d.ts +56 -0
- package/node_modules/pi-typesafe/dist/auth.js +114 -0
- package/node_modules/pi-typesafe/dist/batch.d.ts +74 -0
- package/node_modules/pi-typesafe/dist/batch.js +116 -0
- package/node_modules/pi-typesafe/dist/calibrate.d.ts +106 -0
- package/node_modules/pi-typesafe/dist/calibrate.js +157 -0
- package/node_modules/pi-typesafe/dist/client.d.ts +69 -0
- package/node_modules/pi-typesafe/dist/client.js +188 -0
- package/node_modules/pi-typesafe/dist/credentials.d.ts +46 -0
- package/node_modules/pi-typesafe/dist/credentials.js +112 -0
- package/node_modules/pi-typesafe/dist/errors.d.ts +9 -0
- package/node_modules/pi-typesafe/dist/errors.js +31 -0
- package/node_modules/pi-typesafe/dist/extension.d.ts +3 -0
- package/node_modules/pi-typesafe/dist/extension.js +222 -0
- package/node_modules/pi-typesafe/dist/index.d.ts +18 -0
- package/node_modules/pi-typesafe/dist/index.js +9 -0
- package/node_modules/pi-typesafe/dist/key-prompt.d.ts +3 -0
- package/node_modules/pi-typesafe/dist/key-prompt.js +43 -0
- package/node_modules/pi-typesafe/dist/login.d.ts +27 -0
- package/node_modules/pi-typesafe/dist/login.js +38 -0
- package/node_modules/pi-typesafe/dist/schema.d.ts +43 -0
- package/node_modules/pi-typesafe/dist/schema.js +152 -0
- package/node_modules/pi-typesafe/dist/ui.d.ts +3 -0
- package/node_modules/pi-typesafe/dist/ui.js +3 -0
- package/node_modules/pi-typesafe/dist/usage.d.ts +73 -0
- package/node_modules/pi-typesafe/dist/usage.js +182 -0
- package/node_modules/pi-typesafe/examples/decision-extension.ts +41 -0
- package/node_modules/pi-typesafe/extensions/index.js +2 -0
- package/node_modules/pi-typesafe/package.json +89 -0
- package/package.json +16 -9
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { credentialsPath, keySituation, keySourceLabel, piTypesafeDir } from "./credentials.js";
|
|
4
|
+
import { TypeSafeIntegrationError } from "./errors.js";
|
|
5
|
+
const AUTH_VERSION = 1;
|
|
6
|
+
const CODES = new Set([
|
|
7
|
+
"configuration", "validation", "budget", "aborted", "timeout", "http", "connection", "response",
|
|
8
|
+
]);
|
|
9
|
+
/** Statuses that mean the key itself was refused, not that the service was busy. */
|
|
10
|
+
const REJECTED_STATUSES = new Set([401, 403]);
|
|
11
|
+
/** The auth-state file: one small, owner-only record that outlives the process that wrote it. */
|
|
12
|
+
export function authStatePath() {
|
|
13
|
+
return join(piTypesafeDir(), "auth-state.json");
|
|
14
|
+
}
|
|
15
|
+
function readState(path) {
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
18
|
+
if (!parsed || typeof parsed !== "object")
|
|
19
|
+
return {};
|
|
20
|
+
const raw = parsed;
|
|
21
|
+
const verifiedAt = typeof raw.verifiedAt === "string" && raw.verifiedAt.length <= 40 ? raw.verifiedAt : undefined;
|
|
22
|
+
const failure = raw.lastFailure && typeof raw.lastFailure === "object" ? raw.lastFailure : undefined;
|
|
23
|
+
const code = typeof failure?.code === "string" && CODES.has(failure.code) ? failure.code : undefined;
|
|
24
|
+
const message = typeof failure?.message === "string" ? failure.message.slice(0, 300) : undefined;
|
|
25
|
+
const at = typeof failure?.at === "string" && failure.at.length <= 40 ? failure.at : undefined;
|
|
26
|
+
const status = typeof failure?.status === "number" && Number.isSafeInteger(failure.status) ? failure.status : undefined;
|
|
27
|
+
const lastFailure = code && message && at
|
|
28
|
+
? { code, message, at, ...(status === undefined ? {} : { status }) }
|
|
29
|
+
: undefined;
|
|
30
|
+
return { ...(verifiedAt === undefined ? {} : { verifiedAt }), ...(lastFailure === undefined ? {} : { lastFailure }) };
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Owner-only, atomic, and best-effort: an unwritable auth record never changes how a request behaves. */
|
|
37
|
+
function writeState(path, state) {
|
|
38
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
39
|
+
try {
|
|
40
|
+
mkdirSync(piTypesafeDir(), { recursive: true, mode: 0o700 });
|
|
41
|
+
writeFileSync(temporary, `${JSON.stringify({ version: AUTH_VERSION, ...state }, null, 2)}\n`, { mode: 0o600, flag: "w" });
|
|
42
|
+
chmodSync(temporary, 0o600);
|
|
43
|
+
renameSync(temporary, path);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
try {
|
|
47
|
+
rmSync(temporary, { force: true });
|
|
48
|
+
}
|
|
49
|
+
catch { /* best-effort cleanup only */ }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** What the key situation, the last outcome, and the clock add up to. Never throws. */
|
|
53
|
+
export function authState(options = {}) {
|
|
54
|
+
const path = options.path ?? authStatePath();
|
|
55
|
+
const situation = keySituation();
|
|
56
|
+
const stored = readState(path);
|
|
57
|
+
const source = situation.kind === "environment" ? "environment" : situation.kind === "stored" ? "stored" : undefined;
|
|
58
|
+
const rejected = stored.lastFailure?.code === "http" && stored.lastFailure.status !== undefined && REJECTED_STATUSES.has(stored.lastFailure.status);
|
|
59
|
+
const usable = source !== undefined && !rejected;
|
|
60
|
+
return {
|
|
61
|
+
kind: situation.kind,
|
|
62
|
+
...(source === undefined ? {} : { source }),
|
|
63
|
+
path: situation.kind === "unusable" ? situation.path : credentialsPath(),
|
|
64
|
+
...(situation.kind === "unusable" ? { reason: situation.reason } : {}),
|
|
65
|
+
keyName: keySourceLabel(situation),
|
|
66
|
+
verified: stored.verifiedAt !== undefined && !rejected,
|
|
67
|
+
...(stored.verifiedAt === undefined ? {} : { verifiedAt: stored.verifiedAt }),
|
|
68
|
+
...(stored.lastFailure === undefined ? {} : { lastFailure: stored.lastFailure }),
|
|
69
|
+
usable,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** Record that the key was accepted: login verification, or any successful request. Clears the last failure. */
|
|
73
|
+
export function recordAuthVerified(at = new Date()) {
|
|
74
|
+
writeState(authStatePath(), { verifiedAt: at.toISOString() });
|
|
75
|
+
}
|
|
76
|
+
/** Record the failure that degraded TypeSafe. The verification timestamp is kept so a recovered key stays known. */
|
|
77
|
+
export function recordAuthFailure(error, at = new Date()) {
|
|
78
|
+
const current = readState(authStatePath());
|
|
79
|
+
const failure = {
|
|
80
|
+
code: error.code,
|
|
81
|
+
message: error.message,
|
|
82
|
+
at: at.toISOString(),
|
|
83
|
+
...(error.status === undefined ? {} : { status: error.status }),
|
|
84
|
+
};
|
|
85
|
+
writeState(authStatePath(), { ...(current.verifiedAt === undefined ? {} : { verifiedAt: current.verifiedAt }), lastFailure: failure });
|
|
86
|
+
}
|
|
87
|
+
/** Forget verification and degradation: used when the key itself changes (login or logout). */
|
|
88
|
+
export function clearAuthState() {
|
|
89
|
+
try {
|
|
90
|
+
rmSync(authStatePath(), { force: true });
|
|
91
|
+
}
|
|
92
|
+
catch { /* nothing to clear */ }
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* One line plus a level, so a status command, a headless log, or a consumer's own status line can call out a degraded
|
|
96
|
+
* state instead of reporting "enabled".
|
|
97
|
+
*/
|
|
98
|
+
export function describeAuth(state = authState()) {
|
|
99
|
+
const since = state.lastFailure ? ` Last failure: ${state.lastFailure.message}${state.lastFailure.at ? ` (${state.lastFailure.at})` : ""}` : "";
|
|
100
|
+
if (state.kind === "missing") {
|
|
101
|
+
return { level: "error", text: `TypeSafe key: missing — every Jev judgment is skipped until a key is configured (/typesafe login or TYPESAFE_API_KEY).${since}` };
|
|
102
|
+
}
|
|
103
|
+
if (state.kind === "unusable") {
|
|
104
|
+
return { level: "error", text: `TypeSafe key: unusable (${state.reason ?? "unknown reason"}) — judgments are skipped until the key is fixed.${since}` };
|
|
105
|
+
}
|
|
106
|
+
const rejected = state.lastFailure?.code === "http" && state.lastFailure.status !== undefined && REJECTED_STATUSES.has(state.lastFailure.status);
|
|
107
|
+
if (rejected) {
|
|
108
|
+
return { level: "error", text: `TypeSafe key: ${state.keyName} was rejected.${since}` };
|
|
109
|
+
}
|
|
110
|
+
if (!state.verified) {
|
|
111
|
+
return { level: "warning", text: `TypeSafe key: ${state.keyName} (not verified yet — the first request proves it).${since}` };
|
|
112
|
+
}
|
|
113
|
+
return { level: "ok", text: `TypeSafe key: ${state.keyName} (verified${state.verifiedAt ? ` ${state.verifiedAt}` : ""}).${since}` };
|
|
114
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Questions, SystemOneRequest, Usage } from "@typesafe-ai/sdk";
|
|
2
|
+
import type { Evaluation, TypeSafe } from "./client.js";
|
|
3
|
+
/** Default number of requests in flight. TypeSafe answers in isolation, so a small pool is enough. */
|
|
4
|
+
export declare const DEFAULT_CONCURRENCY = 4;
|
|
5
|
+
/** One item's outcome. `skipped` marks work that was never started because of an abort or a stop rule. */
|
|
6
|
+
export type Settled<T> = {
|
|
7
|
+
readonly ok: true;
|
|
8
|
+
readonly index: number;
|
|
9
|
+
readonly value: T;
|
|
10
|
+
} | {
|
|
11
|
+
readonly ok: false;
|
|
12
|
+
readonly index: number;
|
|
13
|
+
readonly error: unknown;
|
|
14
|
+
readonly skipped: boolean;
|
|
15
|
+
};
|
|
16
|
+
export interface FanOutOptions {
|
|
17
|
+
/** Requests in flight at once. Default: DEFAULT_CONCURRENCY. */
|
|
18
|
+
concurrency?: number;
|
|
19
|
+
/** Stops starting new work once aborted; in-flight work still finishes. */
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
/** Stop launching new work once this returns true for a failure, e.g. a `budget` error. */
|
|
22
|
+
stopOn?: (error: unknown) => boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Run `worker` over `items` with bounded concurrency, preserving input order. Never throws: every item comes back as a
|
|
26
|
+
* settled result. This is the pool the client's batching methods use, exported so script authors stop hand-rolling one.
|
|
27
|
+
*/
|
|
28
|
+
export declare function fanOut<I, O>(items: readonly I[], worker: (item: I, index: number) => Promise<O>, options?: FanOutOptions): Promise<Settled<O>[]>;
|
|
29
|
+
export interface BatchOptions {
|
|
30
|
+
/** Requests in flight at once. Default: DEFAULT_CONCURRENCY. */
|
|
31
|
+
concurrency?: number;
|
|
32
|
+
signal?: AbortSignal;
|
|
33
|
+
}
|
|
34
|
+
/** Per-request outcomes plus the merged view callers usually want. */
|
|
35
|
+
export interface BatchEvaluation<Q extends Questions = Questions> {
|
|
36
|
+
/** True when every request succeeded. */
|
|
37
|
+
readonly ok: boolean;
|
|
38
|
+
/** Per-request outcomes in input order. */
|
|
39
|
+
readonly results: readonly Settled<Evaluation<Q>>[];
|
|
40
|
+
/** Failures, including requests that were never submitted. */
|
|
41
|
+
readonly failures: number;
|
|
42
|
+
/** Work never started, because of an abort or a `budget` stop. */
|
|
43
|
+
readonly skipped: number;
|
|
44
|
+
/** Answers merged in input order; a repeated question id keeps the last answer. Empty when nothing succeeded. */
|
|
45
|
+
readonly answers: Partial<Evaluation<Q>["answers"]>;
|
|
46
|
+
/** The model of the first successful request, when there is one. */
|
|
47
|
+
readonly model?: string;
|
|
48
|
+
/** Usage summed over the requests that succeeded. */
|
|
49
|
+
readonly usage: Usage;
|
|
50
|
+
/** Wall-clock time for the whole batch. */
|
|
51
|
+
readonly elapsedMs: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Send several requests with bounded concurrency, in input order, and merge what came back. Each request passes through
|
|
55
|
+
* the same admission seam as `evaluate`, so an invalid request is one settled failure, not a thrown error. A `budget` or
|
|
56
|
+
* cancellation failure stops the rest from being submitted. Never throws.
|
|
57
|
+
*/
|
|
58
|
+
export declare function evaluateMany<Q extends Questions>(client: TypeSafe, requests: readonly SystemOneRequest<Q>[], options?: BatchOptions): Promise<BatchEvaluation<Q>>;
|
|
59
|
+
/**
|
|
60
|
+
* Split a request that asks more questions than one request may carry into chunks of at most `maxQuestions`. Sharing
|
|
61
|
+
* one state across several questions is one request; asking more than the per-request limit is the only reason to fan
|
|
62
|
+
* out, and the state is repeated in each chunk. The order of `questions` is preserved. A pure splitter: admission still
|
|
63
|
+
* happens once per chunk, in `evaluate` or `evaluateMany`, so what one accepts the others accept.
|
|
64
|
+
*/
|
|
65
|
+
export declare function chunkEvaluationRequest(request: SystemOneRequest<Questions>, options?: {
|
|
66
|
+
maxQuestions?: number;
|
|
67
|
+
}): SystemOneRequest<Questions>[];
|
|
68
|
+
/**
|
|
69
|
+
* Ask any number of questions about one state: chunk to the per-request limit, fan out, and merge the answers, usage,
|
|
70
|
+
* and model. Use this when one coherent state carries many independent questions; use `evaluate` for one request.
|
|
71
|
+
*/
|
|
72
|
+
export declare function evaluateAll<Q extends Questions>(client: TypeSafe, request: SystemOneRequest<Q>, options?: BatchOptions & {
|
|
73
|
+
maxQuestions?: number;
|
|
74
|
+
}): Promise<BatchEvaluation<Q>>;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { TypeSafeIntegrationError } from "./errors.js";
|
|
2
|
+
import { DEFAULT_MAX_QUESTIONS, prepareEvaluationRequest } from "./schema.js";
|
|
3
|
+
/** Default number of requests in flight. TypeSafe answers in isolation, so a small pool is enough. */
|
|
4
|
+
export const DEFAULT_CONCURRENCY = 4;
|
|
5
|
+
/**
|
|
6
|
+
* Run `worker` over `items` with bounded concurrency, preserving input order. Never throws: every item comes back as a
|
|
7
|
+
* settled result. This is the pool the client's batching methods use, exported so script authors stop hand-rolling one.
|
|
8
|
+
*/
|
|
9
|
+
export async function fanOut(items, worker, options = {}) {
|
|
10
|
+
const concurrency = Math.max(1, Math.floor(options.concurrency ?? DEFAULT_CONCURRENCY));
|
|
11
|
+
const results = new Array(items.length);
|
|
12
|
+
let next = 0;
|
|
13
|
+
let stopped = false;
|
|
14
|
+
const run = async () => {
|
|
15
|
+
while (!stopped) {
|
|
16
|
+
const index = next++;
|
|
17
|
+
if (index >= items.length)
|
|
18
|
+
return;
|
|
19
|
+
if (options.signal?.aborted) {
|
|
20
|
+
stopped = true;
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
results[index] = { ok: true, index, value: await worker(items[index], index) };
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
results[index] = { ok: false, index, error, skipped: false };
|
|
28
|
+
if (options.stopOn?.(error))
|
|
29
|
+
stopped = true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, run));
|
|
34
|
+
const reason = options.signal?.aborted ? "TypeSafe batch cancelled before this request was submitted." : "TypeSafe batch stopped after a failed request; this request was not submitted.";
|
|
35
|
+
for (let index = 0; index < items.length; index++) {
|
|
36
|
+
if (!results[index])
|
|
37
|
+
results[index] = { ok: false, index, error: new TypeSafeIntegrationError("aborted", reason), skipped: true };
|
|
38
|
+
}
|
|
39
|
+
return results;
|
|
40
|
+
}
|
|
41
|
+
function summarize(results, elapsedMs) {
|
|
42
|
+
const answers = {};
|
|
43
|
+
let inputTokens = 0;
|
|
44
|
+
let outputTokens = 0;
|
|
45
|
+
let succeeded = 0;
|
|
46
|
+
let skipped = 0;
|
|
47
|
+
let model;
|
|
48
|
+
for (const result of results) {
|
|
49
|
+
if (!result.ok) {
|
|
50
|
+
if (result.skipped)
|
|
51
|
+
skipped++;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
succeeded++;
|
|
55
|
+
Object.assign(answers, result.value.answers);
|
|
56
|
+
inputTokens += result.value.usage.input_tokens;
|
|
57
|
+
outputTokens += result.value.usage.output_tokens;
|
|
58
|
+
model ??= result.value.model;
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
ok: succeeded === results.length,
|
|
62
|
+
results,
|
|
63
|
+
failures: results.length - succeeded,
|
|
64
|
+
skipped,
|
|
65
|
+
answers,
|
|
66
|
+
...(model === undefined ? {} : { model }),
|
|
67
|
+
usage: { input_tokens: inputTokens, output_tokens: outputTokens },
|
|
68
|
+
elapsedMs,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** A failure that is worth stopping the batch for: no more requests will be accepted, or the caller cancelled. */
|
|
72
|
+
function stopsBatch(error) {
|
|
73
|
+
return error instanceof TypeSafeIntegrationError && (error.code === "budget" || error.code === "aborted");
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Send several requests with bounded concurrency, in input order, and merge what came back. Each request passes through
|
|
77
|
+
* the same admission seam as `evaluate`, so an invalid request is one settled failure, not a thrown error. A `budget` or
|
|
78
|
+
* cancellation failure stops the rest from being submitted. Never throws.
|
|
79
|
+
*/
|
|
80
|
+
export async function evaluateMany(client, requests, options = {}) {
|
|
81
|
+
const start = performance.now();
|
|
82
|
+
const results = await fanOut(requests, (request) => client.evaluate(prepareEvaluationRequest(request), options.signal ? { signal: options.signal } : {}), {
|
|
83
|
+
...(options.concurrency === undefined ? {} : { concurrency: options.concurrency }),
|
|
84
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
85
|
+
stopOn: stopsBatch,
|
|
86
|
+
});
|
|
87
|
+
return summarize(results, Math.round(performance.now() - start));
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Split a request that asks more questions than one request may carry into chunks of at most `maxQuestions`. Sharing
|
|
91
|
+
* one state across several questions is one request; asking more than the per-request limit is the only reason to fan
|
|
92
|
+
* out, and the state is repeated in each chunk. The order of `questions` is preserved. A pure splitter: admission still
|
|
93
|
+
* happens once per chunk, in `evaluate` or `evaluateMany`, so what one accepts the others accept.
|
|
94
|
+
*/
|
|
95
|
+
export function chunkEvaluationRequest(request, options = {}) {
|
|
96
|
+
const limit = Math.max(1, Math.floor(options.maxQuestions ?? DEFAULT_MAX_QUESTIONS));
|
|
97
|
+
const questions = request.questions;
|
|
98
|
+
const entries = questions && typeof questions === "object" && !Array.isArray(questions) ? Object.entries(questions) : undefined;
|
|
99
|
+
// Anything that is not a plain question map, or that already fits, is one chunk and is validated later.
|
|
100
|
+
if (!entries || entries.length <= limit)
|
|
101
|
+
return [request];
|
|
102
|
+
const chunks = [];
|
|
103
|
+
for (let index = 0; index < entries.length; index += limit) {
|
|
104
|
+
const questions = Object.fromEntries(entries.slice(index, index + limit));
|
|
105
|
+
chunks.push({ ...request, questions });
|
|
106
|
+
}
|
|
107
|
+
return chunks;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Ask any number of questions about one state: chunk to the per-request limit, fan out, and merge the answers, usage,
|
|
111
|
+
* and model. Use this when one coherent state carries many independent questions; use `evaluate` for one request.
|
|
112
|
+
*/
|
|
113
|
+
export async function evaluateAll(client, request, options = {}) {
|
|
114
|
+
const chunks = chunkEvaluationRequest(request, options.maxQuestions === undefined ? {} : { maxQuestions: options.maxQuestions });
|
|
115
|
+
return evaluateMany(client, chunks, options);
|
|
116
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A judge-tuning kit: label a set of cases, score them with Jev, and read off AUC and threshold behaviour. It carries
|
|
3
|
+
* no domain knowledge — a case is anything a scorer can turn into a number — so the same toolkit fits an action guard,
|
|
4
|
+
* a triage rule, or a prose check. `scripts/calibrate-action.mjs` in pi-warden is the worked example it came from.
|
|
5
|
+
*/
|
|
6
|
+
/** One labelled observation: the truth about the case, and the number the judge assigned to it. */
|
|
7
|
+
export interface ScoredSample {
|
|
8
|
+
readonly label: boolean;
|
|
9
|
+
readonly score: number;
|
|
10
|
+
/** Optional name; used in the missed and flagged listings. */
|
|
11
|
+
readonly id?: string;
|
|
12
|
+
}
|
|
13
|
+
/** Outcome counts for one threshold. `precision` and `recall` are undefined when their denominator is empty. */
|
|
14
|
+
export interface ThresholdRow {
|
|
15
|
+
readonly threshold: number;
|
|
16
|
+
readonly flagged: number;
|
|
17
|
+
readonly tp: number;
|
|
18
|
+
readonly fp: number;
|
|
19
|
+
readonly fn: number;
|
|
20
|
+
readonly tn: number;
|
|
21
|
+
readonly precision?: number;
|
|
22
|
+
readonly recall?: number;
|
|
23
|
+
/** Share of all cases the threshold selects. */
|
|
24
|
+
readonly flagRate: number;
|
|
25
|
+
}
|
|
26
|
+
export interface Calibration {
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly scored: number;
|
|
29
|
+
readonly positives: number;
|
|
30
|
+
readonly negatives: number;
|
|
31
|
+
readonly errors: number;
|
|
32
|
+
/** Rank-based AUC (Mann–Whitney, ties count half); undefined when one class is empty. */
|
|
33
|
+
readonly auc?: number;
|
|
34
|
+
readonly rows: readonly ThresholdRow[];
|
|
35
|
+
/** The lowest threshold meeting the requested precision and recall floors, when one exists. */
|
|
36
|
+
readonly recommended?: ThresholdRow;
|
|
37
|
+
/** Positive cases the recommended threshold misses. */
|
|
38
|
+
readonly missed: readonly ScoredSample[];
|
|
39
|
+
/** Negative cases the recommended threshold flags. */
|
|
40
|
+
readonly flagged: readonly ScoredSample[];
|
|
41
|
+
}
|
|
42
|
+
export interface CalibrateOptions {
|
|
43
|
+
/** Thresholds to evaluate. Default: every distinct score, ascending (at most 64 rows). */
|
|
44
|
+
thresholds?: readonly number[];
|
|
45
|
+
/** Precision floor for the recommendation. */
|
|
46
|
+
minPrecision?: number;
|
|
47
|
+
/** Recall floor for the recommendation. */
|
|
48
|
+
minRecall?: number;
|
|
49
|
+
/** Cases that could not be scored; reported and excluded from the metrics. */
|
|
50
|
+
errors?: number;
|
|
51
|
+
}
|
|
52
|
+
export declare function auc(samples: readonly ScoredSample[]): number | undefined;
|
|
53
|
+
/** Counts at one threshold: a case is flagged when its score is at least the threshold. */
|
|
54
|
+
export declare function metricsAt(samples: readonly ScoredSample[], threshold: number): ThresholdRow;
|
|
55
|
+
export declare function sweep(samples: readonly ScoredSample[], thresholds: readonly number[]): ThresholdRow[];
|
|
56
|
+
/** The distinct scores, ascending, as a threshold grid: every point where the counts can change. */
|
|
57
|
+
export declare function defaultThresholds(samples: readonly ScoredSample[], limit?: number): number[];
|
|
58
|
+
/**
|
|
59
|
+
* The lowest threshold that clears the precision and recall floors. With no floors, the best F1 among the rows;
|
|
60
|
+
* undefined when nothing clears them.
|
|
61
|
+
*/
|
|
62
|
+
export declare function pickThreshold(rows: readonly ThresholdRow[], options?: {
|
|
63
|
+
minPrecision?: number;
|
|
64
|
+
minRecall?: number;
|
|
65
|
+
}): ThresholdRow | undefined;
|
|
66
|
+
/** Label, score, and read the numbers: AUC, the threshold sweep, and one recommendation. */
|
|
67
|
+
export declare function calibrate(name: string, samples: readonly ScoredSample[], options?: CalibrateOptions): Calibration;
|
|
68
|
+
/** Plain text, no colour: safe to write to a report file or a log. */
|
|
69
|
+
export declare function formatCalibration(calibration: Calibration): string;
|
|
70
|
+
/** One labelled replay case: the truth, plus whatever the scorer needs to judge it. */
|
|
71
|
+
export interface ReplayCase<T> {
|
|
72
|
+
readonly id: string;
|
|
73
|
+
readonly label: boolean;
|
|
74
|
+
readonly data: T;
|
|
75
|
+
}
|
|
76
|
+
export interface ReplayResult<T> {
|
|
77
|
+
readonly id: string;
|
|
78
|
+
readonly label: boolean;
|
|
79
|
+
readonly data: T;
|
|
80
|
+
/** The judge's number, absent when the case could not be scored. */
|
|
81
|
+
readonly score?: number;
|
|
82
|
+
/** The failure message, absent on success. Carries no upstream body. */
|
|
83
|
+
readonly error?: string;
|
|
84
|
+
/** True when the case was never submitted (abort or a stopped batch). */
|
|
85
|
+
readonly skipped: boolean;
|
|
86
|
+
}
|
|
87
|
+
export interface ReplayOptions {
|
|
88
|
+
/** Cases in flight at once. Default: DEFAULT_CONCURRENCY (4). */
|
|
89
|
+
concurrency?: number;
|
|
90
|
+
signal?: AbortSignal;
|
|
91
|
+
/** Stop launching new cases once this returns true for a failure, e.g. a `budget` error. */
|
|
92
|
+
stopOn?: (error: unknown) => boolean;
|
|
93
|
+
/** Turns a thrown scorer error into the reported message. Default: the error's own message. */
|
|
94
|
+
describeError?: (error: unknown) => string;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Replay labelled cases through a scorer with bounded concurrency, keeping order and capturing per-case failures.
|
|
98
|
+
* The scorer is usually one Jev question; a thrown error is recorded rather than aborting the run, so one bad case
|
|
99
|
+
* cannot destroy a long calibration. Results feed straight into `samplesOf` and `calibrate`.
|
|
100
|
+
*/
|
|
101
|
+
export declare function replay<T>(cases: readonly ReplayCase<T>[], score: (data: T, index: number) => Promise<number>, options?: ReplayOptions): Promise<ReplayResult<T>[]>;
|
|
102
|
+
/** The scored cases of a replay, in replay order. Unscored cases are excluded and counted as errors. */
|
|
103
|
+
export declare function samplesOf<T>(results: readonly ReplayResult<T>[]): {
|
|
104
|
+
samples: ScoredSample[];
|
|
105
|
+
errors: number;
|
|
106
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { fanOut } from "./batch.js";
|
|
2
|
+
export function auc(samples) {
|
|
3
|
+
const positives = samples.filter(sample => sample.label).map(sample => sample.score);
|
|
4
|
+
const negatives = samples.filter(sample => !sample.label).map(sample => sample.score);
|
|
5
|
+
if (!positives.length || !negatives.length)
|
|
6
|
+
return undefined;
|
|
7
|
+
let wins = 0;
|
|
8
|
+
for (const positive of positives) {
|
|
9
|
+
for (const negative of negatives)
|
|
10
|
+
wins += positive > negative ? 1 : positive === negative ? 0.5 : 0;
|
|
11
|
+
}
|
|
12
|
+
return wins / (positives.length * negatives.length);
|
|
13
|
+
}
|
|
14
|
+
function ratio(numerator, denominator) {
|
|
15
|
+
return denominator ? numerator / denominator : undefined;
|
|
16
|
+
}
|
|
17
|
+
/** Counts at one threshold: a case is flagged when its score is at least the threshold. */
|
|
18
|
+
export function metricsAt(samples, threshold) {
|
|
19
|
+
let tp = 0, fp = 0, fn = 0, tn = 0;
|
|
20
|
+
for (const sample of samples) {
|
|
21
|
+
const flagged = sample.score >= threshold;
|
|
22
|
+
if (flagged && sample.label)
|
|
23
|
+
tp++;
|
|
24
|
+
else if (flagged)
|
|
25
|
+
fp++;
|
|
26
|
+
else if (sample.label)
|
|
27
|
+
fn++;
|
|
28
|
+
else
|
|
29
|
+
tn++;
|
|
30
|
+
}
|
|
31
|
+
const flagged = tp + fp;
|
|
32
|
+
const precision = ratio(tp, tp + fp);
|
|
33
|
+
const recall = ratio(tp, tp + fn);
|
|
34
|
+
return {
|
|
35
|
+
threshold,
|
|
36
|
+
flagged,
|
|
37
|
+
tp,
|
|
38
|
+
fp,
|
|
39
|
+
fn,
|
|
40
|
+
tn,
|
|
41
|
+
...(precision === undefined ? {} : { precision }),
|
|
42
|
+
...(recall === undefined ? {} : { recall }),
|
|
43
|
+
flagRate: samples.length ? flagged / samples.length : 0,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function sweep(samples, thresholds) {
|
|
47
|
+
return thresholds.map(threshold => metricsAt(samples, threshold));
|
|
48
|
+
}
|
|
49
|
+
/** The distinct scores, ascending, as a threshold grid: every point where the counts can change. */
|
|
50
|
+
export function defaultThresholds(samples, limit = 64) {
|
|
51
|
+
const distinct = [...new Set(samples.map(sample => sample.score))].sort((a, b) => a - b);
|
|
52
|
+
if (distinct.length <= limit)
|
|
53
|
+
return distinct;
|
|
54
|
+
const step = (distinct.length - 1) / (limit - 1);
|
|
55
|
+
return Array.from({ length: limit }, (_, index) => distinct[Math.round(index * step)]);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The lowest threshold that clears the precision and recall floors. With no floors, the best F1 among the rows;
|
|
59
|
+
* undefined when nothing clears them.
|
|
60
|
+
*/
|
|
61
|
+
export function pickThreshold(rows, options = {}) {
|
|
62
|
+
const { minPrecision, minRecall } = options;
|
|
63
|
+
if (minPrecision === undefined && minRecall === undefined) {
|
|
64
|
+
let best;
|
|
65
|
+
let bestF1 = -1;
|
|
66
|
+
for (const row of rows) {
|
|
67
|
+
if (row.precision === undefined || row.recall === undefined)
|
|
68
|
+
continue;
|
|
69
|
+
const f1 = row.precision + row.recall === 0 ? 0 : 2 * row.precision * row.recall / (row.precision + row.recall);
|
|
70
|
+
if (f1 > bestF1) {
|
|
71
|
+
bestF1 = f1;
|
|
72
|
+
best = row;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return best;
|
|
76
|
+
}
|
|
77
|
+
const candidates = rows
|
|
78
|
+
.filter(row => (minPrecision === undefined || (row.precision ?? 0) >= minPrecision) && (minRecall === undefined || (row.recall ?? 0) >= minRecall))
|
|
79
|
+
.sort((a, b) => a.threshold - b.threshold);
|
|
80
|
+
return candidates[0];
|
|
81
|
+
}
|
|
82
|
+
/** Label, score, and read the numbers: AUC, the threshold sweep, and one recommendation. */
|
|
83
|
+
export function calibrate(name, samples, options = {}) {
|
|
84
|
+
const thresholds = options.thresholds ?? defaultThresholds(samples);
|
|
85
|
+
const rows = sweep(samples, thresholds);
|
|
86
|
+
const recommendation = pickThreshold(rows, {
|
|
87
|
+
...(options.minPrecision === undefined ? {} : { minPrecision: options.minPrecision }),
|
|
88
|
+
...(options.minRecall === undefined ? {} : { minRecall: options.minRecall }),
|
|
89
|
+
});
|
|
90
|
+
const rank = auc(samples);
|
|
91
|
+
const threshold = recommendation?.threshold;
|
|
92
|
+
return {
|
|
93
|
+
name,
|
|
94
|
+
scored: samples.length,
|
|
95
|
+
positives: samples.filter(sample => sample.label).length,
|
|
96
|
+
negatives: samples.filter(sample => !sample.label).length,
|
|
97
|
+
errors: options.errors ?? 0,
|
|
98
|
+
...(rank === undefined ? {} : { auc: rank }),
|
|
99
|
+
rows,
|
|
100
|
+
...(recommendation === undefined ? {} : { recommended: recommendation }),
|
|
101
|
+
missed: threshold === undefined ? [] : samples.filter(sample => sample.label && sample.score < threshold),
|
|
102
|
+
flagged: threshold === undefined ? [] : samples.filter(sample => !sample.label && sample.score >= threshold),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const percent = (value) => value === undefined ? "-" : `${(value * 100).toFixed(0)}%`;
|
|
106
|
+
/** Plain text, no colour: safe to write to a report file or a log. */
|
|
107
|
+
export function formatCalibration(calibration) {
|
|
108
|
+
const lines = [
|
|
109
|
+
`${calibration.name}: ${calibration.scored} scored, ${calibration.positives} positives, ${calibration.negatives} negatives${calibration.errors ? `, ${calibration.errors} errors` : ""}`,
|
|
110
|
+
`AUC ${calibration.auc === undefined ? "-" : calibration.auc.toFixed(3)}`,
|
|
111
|
+
"threshold flagged TP FP FN TN precision recall",
|
|
112
|
+
];
|
|
113
|
+
for (const row of calibration.rows) {
|
|
114
|
+
lines.push(`${row.threshold.toFixed(2).padStart(9)} ${String(row.flagged).padStart(7)} ${String(row.tp).padStart(2)} ${String(row.fp).padStart(2)} ${String(row.fn).padStart(2)} ${String(row.tn).padStart(2)} ${percent(row.precision).padStart(9)} ${percent(row.recall).padStart(6)}`);
|
|
115
|
+
}
|
|
116
|
+
const recommended = calibration.recommended;
|
|
117
|
+
lines.push(recommended === undefined
|
|
118
|
+
? "recommended: none (no threshold clears the floors)"
|
|
119
|
+
: `recommended ${recommended.threshold.toFixed(2)}: precision ${percent(recommended.precision)}, recall ${percent(recommended.recall)}, flags ${percent(recommended.flagRate)}`);
|
|
120
|
+
if (calibration.missed.length)
|
|
121
|
+
lines.push(`missed positives (${calibration.missed.length}): ${calibration.missed.map(sample => sample.id ?? sample.score.toFixed(2)).join(", ").slice(0, 300)}`);
|
|
122
|
+
if (calibration.flagged.length)
|
|
123
|
+
lines.push(`flagged negatives (${calibration.flagged.length}): ${calibration.flagged.map(sample => sample.id ?? sample.score.toFixed(2)).join(", ").slice(0, 300)}`);
|
|
124
|
+
return lines.join("\n");
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Replay labelled cases through a scorer with bounded concurrency, keeping order and capturing per-case failures.
|
|
128
|
+
* The scorer is usually one Jev question; a thrown error is recorded rather than aborting the run, so one bad case
|
|
129
|
+
* cannot destroy a long calibration. Results feed straight into `samplesOf` and `calibrate`.
|
|
130
|
+
*/
|
|
131
|
+
export async function replay(cases, score, options = {}) {
|
|
132
|
+
const settled = await fanOut(cases, (item, index) => score(item.data, index), {
|
|
133
|
+
...(options.concurrency === undefined ? {} : { concurrency: options.concurrency }),
|
|
134
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
135
|
+
...(options.stopOn === undefined ? {} : { stopOn: options.stopOn }),
|
|
136
|
+
});
|
|
137
|
+
return settled.map((result, index) => {
|
|
138
|
+
const item = cases[index];
|
|
139
|
+
if (result.ok)
|
|
140
|
+
return { id: item.id, label: item.label, data: item.data, score: result.value, skipped: false };
|
|
141
|
+
const error = options.describeError ? options.describeError(result.error) : result.error instanceof Error ? result.error.message : "The scorer failed.";
|
|
142
|
+
return { id: item.id, label: item.label, data: item.data, error, skipped: result.skipped };
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/** The scored cases of a replay, in replay order. Unscored cases are excluded and counted as errors. */
|
|
146
|
+
export function samplesOf(results) {
|
|
147
|
+
const samples = [];
|
|
148
|
+
let errors = 0;
|
|
149
|
+
for (const result of results) {
|
|
150
|
+
if (result.score === undefined || !Number.isFinite(result.score)) {
|
|
151
|
+
errors++;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
samples.push({ label: result.label, score: result.score, id: result.id });
|
|
155
|
+
}
|
|
156
|
+
return { samples, errors };
|
|
157
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Fetch, Questions, SystemOneRequest, SystemOneResult } from "@typesafe-ai/sdk";
|
|
2
|
+
import type { BatchEvaluation, BatchOptions } from "./batch.js";
|
|
3
|
+
import type { BlockedCap, SpendCaps, UsageLedger, UsageReport } from "./usage.js";
|
|
4
|
+
export interface TypeSafeOptions {
|
|
5
|
+
/** Defaults to TYPESAFE_API_KEY, then the key saved by `/typesafe login`; never returned. */
|
|
6
|
+
apiKey?: string;
|
|
7
|
+
/** Defaults to jev-latest. No model is inferred from submitted content. */
|
|
8
|
+
model?: string;
|
|
9
|
+
/** Per request. Default: 15 seconds. No automatic retries. */
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
/** UTF-8 JSON bytes, including model/questions. Default: 64 KiB. Not a token limit. */
|
|
12
|
+
maxInputBytes?: number;
|
|
13
|
+
/** Attempts per client instance, including failed network requests. Default: 20. */
|
|
14
|
+
maxRequests?: number;
|
|
15
|
+
/** Requests per local day, counted across processes and restarts. Unlimited by default. */
|
|
16
|
+
maxRequestsPerDay?: number;
|
|
17
|
+
/** Input tokens per local day. Unlimited by default. */
|
|
18
|
+
maxInputTokensPerDay?: number;
|
|
19
|
+
/** Estimated spend per local day, in US dollars. Unlimited by default. */
|
|
20
|
+
maxUsdPerDay?: number;
|
|
21
|
+
/** Price used for the cost estimate and the USD cap. Default: DEFAULT_USD_PER_MTOK. */
|
|
22
|
+
usdPerMTok?: number;
|
|
23
|
+
/** The usage ledger; defaults to the store next to the key. Injected by tests. */
|
|
24
|
+
ledger?: UsageLedger;
|
|
25
|
+
/** Transport injection for extension authors and offline tests. */
|
|
26
|
+
fetch?: Fetch;
|
|
27
|
+
}
|
|
28
|
+
export interface EvaluationOptions {
|
|
29
|
+
signal?: AbortSignal;
|
|
30
|
+
}
|
|
31
|
+
export type Evaluation<Q extends Questions> = SystemOneResult<Q> & {
|
|
32
|
+
readonly elapsedMs: number;
|
|
33
|
+
};
|
|
34
|
+
/** Session counters for one client instance, plus the cost estimate they add up to. */
|
|
35
|
+
export interface UsageSnapshot {
|
|
36
|
+
readonly requestsStarted: number;
|
|
37
|
+
readonly requestsSucceeded: number;
|
|
38
|
+
readonly requestsFailed: number;
|
|
39
|
+
readonly inputTokens: number;
|
|
40
|
+
readonly outputTokens: number;
|
|
41
|
+
readonly estimatedUsd: number;
|
|
42
|
+
}
|
|
43
|
+
/** Session counters, today's persisted counters, the caps in force, and the cap that is currently reached. */
|
|
44
|
+
export interface SpendReport {
|
|
45
|
+
readonly session: UsageSnapshot;
|
|
46
|
+
readonly today: UsageReport;
|
|
47
|
+
readonly caps: SpendCaps;
|
|
48
|
+
readonly usdPerMTok: number;
|
|
49
|
+
readonly blocked?: BlockedCap;
|
|
50
|
+
}
|
|
51
|
+
export interface TypeSafe {
|
|
52
|
+
evaluate<Q extends Questions>(request: SystemOneRequest<Q>, options?: EvaluationOptions): Promise<Evaluation<Q>>;
|
|
53
|
+
/** Several requests with bounded concurrency; answers, usage, and model merged. Never throws. */
|
|
54
|
+
evaluateMany<Q extends Questions>(requests: readonly SystemOneRequest<Q>[], options?: BatchOptions): Promise<BatchEvaluation<Q>>;
|
|
55
|
+
/** One state, any number of questions: chunk to the per-request limit, then fan out. Never throws. */
|
|
56
|
+
evaluateAll<Q extends Questions>(request: SystemOneRequest<Q>, options?: BatchOptions & {
|
|
57
|
+
maxQuestions?: number;
|
|
58
|
+
}): Promise<BatchEvaluation<Q>>;
|
|
59
|
+
/** Model names available to the account. Verifies the key; does not count toward maxRequests. */
|
|
60
|
+
listModels(options?: EvaluationOptions): Promise<string[]>;
|
|
61
|
+
/** This client's session counters. */
|
|
62
|
+
getUsage(): UsageSnapshot;
|
|
63
|
+
/** Session counters, today's persisted totals, and the caps that stop the next request. */
|
|
64
|
+
getSpend(): SpendReport;
|
|
65
|
+
}
|
|
66
|
+
/** Default attempts per client instance; the extension quotes the same number in its consent copy. */
|
|
67
|
+
export declare const DEFAULT_MAX_REQUESTS = 20;
|
|
68
|
+
/** A bounded, server-side TypeSafe client independent of Pi's runtime. */
|
|
69
|
+
export declare function createTypeSafe(options?: TypeSafeOptions): TypeSafe;
|