pi-advisor-flow 0.6.0 → 0.8.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 (93) hide show
  1. package/CHANGELOG.md +34 -17
  2. package/README.md +10 -8
  3. package/dist/index.js +2852 -868
  4. package/extensions/index.ts +2 -1
  5. package/package.json +55 -40
  6. package/src/attachments.ts +11 -10
  7. package/src/commands/activation-preparation.ts +1 -0
  8. package/src/commands/activation.ts +2 -0
  9. package/src/commands/lifecycle.ts +1 -0
  10. package/src/commands/manual-command.ts +7 -1
  11. package/src/commands/manual-consultation.ts +103 -79
  12. package/src/commands/manual-progress.ts +7 -5
  13. package/src/commands/model-commands.ts +1 -1
  14. package/src/commands/model-options.ts +14 -3
  15. package/src/commands/model-picker.ts +1 -0
  16. package/src/commands/registration.ts +1 -0
  17. package/src/commands/renderers.ts +67 -55
  18. package/src/commands/runtime.ts +16 -14
  19. package/src/commands/settings-commands.ts +10 -4
  20. package/src/commands/settings-persistence.ts +64 -2
  21. package/src/commands/types.ts +2 -0
  22. package/src/commands.ts +0 -1
  23. package/src/config/args.ts +1 -1
  24. package/src/config/defaults.ts +83 -1
  25. package/src/config/schema.ts +152 -15
  26. package/src/config/state.ts +80 -2
  27. package/src/config/storage.ts +38 -22
  28. package/src/config/types.ts +45 -3
  29. package/src/config/validation.ts +33 -21
  30. package/src/config.ts +3 -0
  31. package/src/content-utils.ts +24 -4
  32. package/src/conversation.ts +16 -15
  33. package/src/git.ts +10 -3
  34. package/src/herdr-block.ts +83 -0
  35. package/src/herdr-shared.ts +108 -0
  36. package/src/herdr.ts +30 -192
  37. package/src/jev/client.ts +338 -0
  38. package/src/jev/failure.ts +19 -0
  39. package/src/jev/key-store.ts +267 -0
  40. package/src/jev/ledger.ts +228 -0
  41. package/src/jev/questions.ts +122 -0
  42. package/src/jev/state.ts +50 -0
  43. package/src/jev/transport.ts +63 -0
  44. package/src/model-stream.ts +35 -37
  45. package/src/outcomes.ts +20 -12
  46. package/src/pi-settings.ts +4 -9
  47. package/src/preferences.ts +4 -2
  48. package/src/redaction.ts +10 -12
  49. package/src/scout-context.ts +59 -60
  50. package/src/scout-curation.ts +4 -13
  51. package/src/scout-groups.ts +178 -175
  52. package/src/scout-protocol.ts +74 -70
  53. package/src/scout-reconstruct.ts +1 -1
  54. package/src/scout.ts +115 -73
  55. package/src/session-state.ts +148 -39
  56. package/src/tool-result-cap.ts +18 -17
  57. package/src/tools/consult-context.ts +7 -15
  58. package/src/tools/consultation.ts +14 -4
  59. package/src/tools/gate-policy.ts +1 -0
  60. package/src/tools/gate-protocol.ts +36 -23
  61. package/src/tools/jev-filter.ts +204 -0
  62. package/src/tools/jev-turn-gate.ts +203 -0
  63. package/src/tools/loop-gate.ts +32 -20
  64. package/src/tools/model-access.ts +46 -0
  65. package/src/tools/outage-notifier.ts +30 -0
  66. package/src/tools/prompts.ts +20 -25
  67. package/src/tools/register-ask-advisor.ts +73 -17
  68. package/src/tools/register-lifecycle.ts +22 -0
  69. package/src/tools/register-outcome.ts +2 -0
  70. package/src/tools/register-renderers.ts +149 -73
  71. package/src/tools/registration.ts +15 -0
  72. package/src/tools/render-advisor-result.ts +77 -35
  73. package/src/tools/render-common.ts +16 -19
  74. package/src/tools/scout-status.ts +42 -27
  75. package/src/tools/types.ts +11 -0
  76. package/src/tools.ts +0 -1
  77. package/src/ui/jev-setup-submenu.ts +350 -0
  78. package/src/ui/manual-dialog-render.ts +1 -0
  79. package/src/ui/manual-dialog.ts +39 -43
  80. package/src/ui/masked-input.ts +62 -0
  81. package/src/ui/model-multi-selector.ts +9 -0
  82. package/src/ui/model-selector-adapter.ts +31 -0
  83. package/src/ui/model-selector.ts +14 -161
  84. package/src/ui/searchable-model-list.ts +230 -0
  85. package/src/ui/settings-formatting.ts +24 -14
  86. package/src/ui/settings-items.ts +200 -2
  87. package/src/ui/settings-list-adapter.ts +15 -12
  88. package/src/ui/settings-mutations.ts +169 -47
  89. package/src/ui/settings-selector.ts +18 -9
  90. package/src/ui/text-setting-submenu.ts +3 -6
  91. package/src/ui/types.ts +25 -0
  92. package/src/ui.ts +0 -1
  93. package/src/usage.ts +29 -20
package/src/herdr.ts CHANGED
@@ -1,108 +1,37 @@
1
- import net from "node:net";
2
1
  import { getAdvisorSettings } from "./config/state.ts";
3
- import { redactSecrets } from "./redaction.ts";
4
-
5
- // Keep the JSON-RPC method components separate from Socket's URL-string heuristic.
6
- const HERDR_NOTIFICATION_METHOD = "notification.show";
7
-
8
- const SOURCE = "pi-advisor:advisor-activity";
9
- const BLOCK_SOURCE = "pi-advisor:advisor-block";
10
- const NOTIFICATION_SOURCE = "pi-advisor:advisor-notification";
11
- const HERDR_PI_SOURCE = "herdr:pi";
12
- let sequence = Date.now() * 1000;
13
- const nextSequence = () => {
14
- sequence += 1;
15
- return sequence;
16
- };
17
-
18
- export interface HerdrMetadataRequest {
19
- id: string;
20
- method: "pane.report_metadata";
21
- params: {
22
- pane_id: string;
23
- source: string;
24
- agent: "pi";
25
- applies_to_source: string;
26
- state_labels?: { working?: string; blocked?: string };
27
- clear_state_labels?: true;
28
- seq: number;
29
- };
30
- }
31
- export interface HerdrNotificationRequest {
32
- id: string;
33
- method: typeof HERDR_NOTIFICATION_METHOD;
34
- params: {
35
- title: string;
36
- body: string;
37
- position: "top-left";
38
- sound: "request";
39
- };
40
- }
41
- type HerdrRequest = HerdrMetadataRequest | HerdrNotificationRequest;
42
- type Report = (request: HerdrRequest) => void;
43
-
44
- // Herdr drops socket reports from other sources, so the blocked state must be
45
- // signalled through the in-process pi event bus its integration listens on.
46
- type BlockedEmitter = (active: boolean, label: string) => void;
47
- let emitBlocked: BlockedEmitter | undefined;
48
-
49
- export const setHerdrBlockedEmitter = (emitter: BlockedEmitter | undefined) => {
50
- emitBlocked = emitter;
51
- };
52
-
53
- const safeEmitBlocked = (active: boolean, label = "Advisor blocked") => {
54
- try {
55
- emitBlocked?.(active, label);
56
- } catch {
57
- /* Herdr is optional. */
58
- }
59
- };
60
-
61
- const isControlCharacter = (character: string) =>
62
- character <= "\u001f" || character === "\u007f";
63
-
64
- const cleanNotification = (value: string, max: number) =>
65
- [...redactSecrets(value)]
66
- .map((character) => (isControlCharacter(character) ? " " : character))
67
- .join("")
68
- .replace(/\s+/g, " ")
69
- .trim()
70
- .slice(0, max);
71
-
72
- export const createHerdrNotificationRequest = (
73
- title: string,
74
- body: string
75
- ): HerdrNotificationRequest => ({
76
- id: `${NOTIFICATION_SOURCE}:${nextSequence()}`,
77
- method: HERDR_NOTIFICATION_METHOD,
2
+ import { HerdrAdvisorBlock } from "./herdr-block.ts";
3
+ import type { HerdrMetadataRequest, Report } from "./herdr-shared.ts";
4
+ import {
5
+ createHerdrNotificationRequest,
6
+ HERDR_PI_SOURCE,
7
+ nextSequence,
8
+ sendToHerdr,
9
+ SOURCE,
10
+ } from "./herdr-shared.ts";
11
+
12
+ export { HerdrAdvisorBlock } from "./herdr-block.ts";
13
+ export {
14
+ createHerdrNotificationRequest,
15
+ type HerdrMetadataRequest,
16
+ type HerdrNotificationRequest,
17
+ setHerdrBlockedEmitter,
18
+ } from "./herdr-shared.ts";
19
+
20
+ const metadataRequest = (clear: boolean): HerdrMetadataRequest => ({
21
+ id: `${SOURCE}:${nextSequence()}`,
22
+ method: "pane.report_metadata",
78
23
  params: {
79
- body: cleanNotification(body, 240),
80
- position: "top-left",
81
- sound: "request",
82
- title: cleanNotification(title, 80),
24
+ agent: "pi",
25
+ applies_to_source: HERDR_PI_SOURCE,
26
+ pane_id: process.env.HERDR_PANE_ID ?? "",
27
+ source: SOURCE,
28
+ ...(clear
29
+ ? { clear_state_labels: true }
30
+ : { state_labels: { working: "seeking advice" } }),
31
+ seq: nextSequence(),
83
32
  },
84
33
  });
85
34
 
86
- const sendToHerdr: Report = (request) => {
87
- if (process.env.HERDR_ENV !== "1") {
88
- return;
89
- }
90
- const paneId = process.env.HERDR_PANE_ID;
91
- const socketPath = process.env.HERDR_SOCKET_PATH;
92
- if (!(paneId && socketPath)) {
93
- return;
94
- }
95
- const endpoint =
96
- process.platform === "win32" ? `\\\\.\\pipe\\${socketPath}` : socketPath;
97
- const socket = net.createConnection(endpoint);
98
- const timeout = setTimeout(() => socket.destroy(), 500);
99
- timeout.unref?.();
100
- socket.once("connect", () => socket.write(`${JSON.stringify(request)}\n`));
101
- socket.once("data", () => socket.destroy());
102
- socket.once("error", () => socket.destroy());
103
- socket.once("close", () => clearTimeout(timeout));
104
- };
105
-
106
35
  export class HerdrAdvisorActivity {
107
36
  #activeConsultations = 0;
108
37
  private readonly report: Report;
@@ -146,98 +75,7 @@ export class HerdrAdvisorActivity {
146
75
 
147
76
  private safeReport(clear: boolean) {
148
77
  try {
149
- this.report(this.request(clear));
150
- } catch {
151
- /* Herdr is optional. */
152
- }
153
- }
154
-
155
- private request(clear: boolean): HerdrMetadataRequest {
156
- return {
157
- id: `${SOURCE}:${nextSequence()}`,
158
- method: "pane.report_metadata",
159
- params: {
160
- agent: "pi",
161
- applies_to_source: HERDR_PI_SOURCE,
162
- pane_id: process.env.HERDR_PANE_ID ?? "",
163
- source: SOURCE,
164
- ...(clear
165
- ? { clear_state_labels: true }
166
- : { state_labels: { working: "seeking advice" } }),
167
- seq: nextSequence(),
168
- },
169
- };
170
- }
171
- }
172
-
173
- export class HerdrAdvisorBlock {
174
- #blocked = false;
175
- private readonly report: Report;
176
- private readonly enabled: () => boolean;
177
-
178
- constructor(
179
- report: Report = sendToHerdr,
180
- enabled: () => boolean = () => true
181
- ) {
182
- this.report = report;
183
- this.enabled = enabled;
184
- }
185
-
186
- set(reason: string) {
187
- if (!this.enabled()) {
188
- return;
189
- }
190
- const label = cleanNotification(reason, 200);
191
- const wasBlocked: boolean = this.#blocked;
192
- if (!wasBlocked) {
193
- // The listener refcounts, so only the false → true edge may emit.
194
- safeEmitBlocked(true, label);
195
- }
196
- this.#blocked = true;
197
- this.safeReport({ blocked: label });
198
- }
199
-
200
- clear() {
201
- const wasBlocked: boolean = this.#blocked;
202
- this.#blocked = false;
203
- if (!wasBlocked) {
204
- return;
205
- }
206
- // Clearing previously reported state is a de-escalation and must still be
207
- // delivered if integration was disabled after the block was reported.
208
- safeEmitBlocked(false);
209
- try {
210
- this.report({
211
- id: `${BLOCK_SOURCE}:${nextSequence()}`,
212
- method: "pane.report_metadata",
213
- params: {
214
- agent: "pi",
215
- applies_to_source: HERDR_PI_SOURCE,
216
- clear_state_labels: true,
217
- pane_id: process.env.HERDR_PANE_ID ?? "",
218
- seq: nextSequence(),
219
- source: BLOCK_SOURCE,
220
- },
221
- });
222
- } catch {
223
- /* Herdr is optional. */
224
- }
225
- }
226
-
227
- private safeReport(labels: { blocked: string }) {
228
- try {
229
- this.report({
230
- id: `${BLOCK_SOURCE}:${nextSequence()}`,
231
- method: "pane.report_metadata",
232
- params: {
233
- agent: "pi",
234
- applies_to_source: HERDR_PI_SOURCE,
235
- pane_id: process.env.HERDR_PANE_ID ?? "",
236
- seq: nextSequence(),
237
- source: BLOCK_SOURCE,
238
- state_labels: labels,
239
- },
240
- });
78
+ this.report(metadataRequest(clear));
241
79
  } catch {
242
80
  /* Herdr is optional. */
243
81
  }
@@ -0,0 +1,338 @@
1
+ import type { EntryType, Fetch, Questions } from "@typesafe-ai/sdk";
2
+
3
+ import {
4
+ advisorJevModelRef,
5
+ advisorJevPricePerMtokRef,
6
+ advisorJevTimeoutMsRef,
7
+ } from "../config/state.ts";
8
+ import { isNumber, isRecord, isRecordOf, isString } from "../content-utils.ts";
9
+ import type { JsonValue, RecordValue } from "../content-utils.ts";
10
+ import { redactSecrets } from "../redaction.ts";
11
+ import { JevFailureError } from "./failure.ts";
12
+ import type { JevErrorCategory } from "./failure.ts";
13
+ import type { JevCredentials, JevTransportKind } from "./transport.ts";
14
+
15
+ export type { JevErrorCategory } from "./failure.ts";
16
+ export { JevFailureError as JevFailure } from "./failure.ts";
17
+
18
+ export interface JevUsage {
19
+ cost: number;
20
+ inputTokens: number;
21
+ outputTokens: number;
22
+ }
23
+
24
+ export interface JevAskResult {
25
+ answers: RecordValue;
26
+ model: string;
27
+ usage: JevUsage;
28
+ }
29
+
30
+ export interface JevClientOptions {
31
+ apiKey: string;
32
+ fetch?: Fetch;
33
+ model: string;
34
+ pricePerMtok?: number;
35
+ timeoutMs: number;
36
+ transport: JevTransportKind;
37
+ }
38
+
39
+ const ENDPOINTS: Record<JevTransportKind, string> = {
40
+ openrouter: "https://openrouter.ai/api/alpha/decisions",
41
+ typesafe: "https://api.typesafe.ai/v1/systemone",
42
+ };
43
+
44
+ const range = (from: number, to: number): number[] =>
45
+ Array.from({ length: to - from + 1 }, (_, index) => from + index);
46
+
47
+ const RETRYABLE_STATUSES = new Set([408, 429, ...range(500, 599)]);
48
+ const RETRY_BACKOFF_MS = 250;
49
+ const MAX_ATTEMPTS = 2;
50
+
51
+ const finiteTokens = (value: JsonValue | undefined): number =>
52
+ isNumber(value) && Number.isFinite(value) && value >= 0 ? value : 0;
53
+
54
+ const errorDetail = (error: JsonValue | undefined): string => {
55
+ if (isString(error)) {
56
+ return error;
57
+ }
58
+ if (isRecordOf(error) && "message" in error) {
59
+ return String(error.message);
60
+ }
61
+ return "";
62
+ };
63
+
64
+ const statusCategory = (status: number): JevErrorCategory => {
65
+ if (status === 401 || status === 403) {
66
+ return "auth";
67
+ }
68
+ if (status === 400 || status === 422) {
69
+ return "malformed";
70
+ }
71
+ return "error";
72
+ };
73
+
74
+ const isObjectLike = <Value>(value: Value): value is Value & object =>
75
+ typeof value === "object";
76
+
77
+ const openRouterModelId = (model: string) =>
78
+ model.includes("/") ? model : `~typesafe/${model}`;
79
+
80
+ interface AttemptOutcome {
81
+ answers?: RecordValue;
82
+ failure?: JevFailureError;
83
+ model?: string;
84
+ retryable?: boolean;
85
+ usage?: { input_tokens?: JsonValue; output_tokens?: JsonValue };
86
+ }
87
+
88
+ const connectionFailure = <E>(error: E) => {
89
+ const message = redactSecrets(
90
+ error instanceof Error ? error.message : String(error)
91
+ );
92
+ return {
93
+ failure: new JevFailureError(
94
+ "network",
95
+ `Jev connection failed: ${message}`
96
+ ),
97
+ };
98
+ };
99
+
100
+ const sleepWithAbort = (signal: AbortSignal, ms: number): Promise<void> =>
101
+ // oxlint-disable-next-line promise/avoid-new -- unref-ed abortable retry timer cannot be expressed with async/await.
102
+ new Promise((resolve) => {
103
+ const timer = setTimeout(resolve, ms);
104
+ timer.unref?.();
105
+ signal.addEventListener(
106
+ "abort",
107
+ () => {
108
+ clearTimeout(timer);
109
+ resolve();
110
+ },
111
+ { once: true }
112
+ );
113
+ });
114
+
115
+ /** One systemone client for both transports with a total wall deadline so
116
+ * retries can never stall a tool call; errors are classified and redacted on
117
+ * every path. */
118
+ export class JevClient {
119
+ readonly #apiKey: string;
120
+ readonly #endpoint: string;
121
+ readonly #fetch: Fetch;
122
+ readonly #model: string;
123
+ readonly #pricePerMtok: number | undefined;
124
+ readonly #timeoutMs: number;
125
+
126
+ constructor({
127
+ apiKey,
128
+ fetch,
129
+ model,
130
+ pricePerMtok,
131
+ timeoutMs,
132
+ transport,
133
+ }: JevClientOptions) {
134
+ this.#apiKey = apiKey;
135
+ this.#endpoint = ENDPOINTS[transport];
136
+ // SAFETY: the bound global fetch satisfies the SDK Fetch signature; binding keeps the receiver correct.
137
+ this.#fetch = fetch ?? (globalThis.fetch.bind(globalThis) as Fetch);
138
+ this.#model = transport === "openrouter" ? openRouterModelId(model) : model;
139
+ this.#pricePerMtok = pricePerMtok;
140
+ this.#timeoutMs = timeoutMs;
141
+ }
142
+
143
+ async ask(
144
+ state: EntryType,
145
+ questions: Questions,
146
+ signal?: AbortSignal
147
+ ): Promise<JevAskResult> {
148
+ const deadline = new AbortController();
149
+ const abortFromCaller = () => deadline.abort(signal?.reason);
150
+ signal?.addEventListener("abort", abortFromCaller, { once: true });
151
+ if (signal?.aborted) {
152
+ abortFromCaller();
153
+ }
154
+ let deadlineHit = false;
155
+ const timer = setTimeout(() => {
156
+ deadlineHit = true;
157
+ deadline.abort(new Error("Jev wall-time budget elapsed"));
158
+ }, this.#timeoutMs);
159
+ timer.unref?.();
160
+ const body = JSON.stringify({
161
+ model: this.#model,
162
+ questions,
163
+ state,
164
+ });
165
+ try {
166
+ let outcome: AttemptOutcome = {};
167
+ for (let attempt = 1; ; attempt += 1) {
168
+ // The retry loop is bounded to one backoff by the deadline controller.
169
+ outcome = await this.#attempt(body, deadline.signal);
170
+ if (!outcome.retryable || attempt >= MAX_ATTEMPTS) {
171
+ break;
172
+ }
173
+ await sleepWithAbort(deadline.signal, RETRY_BACKOFF_MS);
174
+ if (deadline.signal.aborted) {
175
+ break;
176
+ }
177
+ }
178
+ return this.#settle(outcome, deadlineHit, signal);
179
+ } catch (error) {
180
+ if (signal?.aborted && !deadlineHit) {
181
+ throw error;
182
+ }
183
+ if (error instanceof JevFailureError) {
184
+ throw error;
185
+ }
186
+ const message = redactSecrets(
187
+ error instanceof Error ? error.message : String(error)
188
+ );
189
+ throw deadlineHit
190
+ ? new JevFailureError(
191
+ "timeout",
192
+ `Jev call exceeded its ${this.#timeoutMs} ms wall-time budget.`
193
+ )
194
+ : new JevFailureError("error", message);
195
+ } finally {
196
+ clearTimeout(timer);
197
+ signal?.removeEventListener("abort", abortFromCaller);
198
+ }
199
+ }
200
+
201
+ #settle(
202
+ outcome: AttemptOutcome,
203
+ deadlineHit: boolean,
204
+ signal: AbortSignal | undefined
205
+ ): JevAskResult {
206
+ if (outcome.answers) {
207
+ return this.#result(outcome);
208
+ }
209
+ if (deadlineHit && outcome.failure?.category !== "auth") {
210
+ throw new JevFailureError(
211
+ "timeout",
212
+ `Jev call exceeded its ${this.#timeoutMs} ms wall-time budget.`
213
+ );
214
+ }
215
+ if (outcome.failure) {
216
+ throw outcome.failure;
217
+ }
218
+ if (signal?.aborted) {
219
+ throw new Error("Jev call aborted by the caller.");
220
+ }
221
+ throw new JevFailureError("error", "Jev call failed.");
222
+ }
223
+
224
+ async #attempt(body: string, signal: AbortSignal): Promise<AttemptOutcome> {
225
+ if (signal.aborted) {
226
+ return { retryable: false };
227
+ }
228
+ let response: Response;
229
+ try {
230
+ response = await this.#fetch(this.#endpoint, {
231
+ body,
232
+ headers: {
233
+ authorization: `Bearer ${this.#apiKey}`,
234
+ "content-type": "application/json",
235
+ },
236
+ method: "POST",
237
+ signal,
238
+ });
239
+ } catch (error) {
240
+ if (signal.aborted) {
241
+ return { retryable: false };
242
+ }
243
+ return {
244
+ retryable: true,
245
+ ...connectionFailure(error),
246
+ };
247
+ }
248
+ if (response.ok) {
249
+ return this.#parseSuccess(response);
250
+ }
251
+ const failure = await this.#failureFromStatus(response);
252
+ return {
253
+ failure,
254
+ retryable: RETRYABLE_STATUSES.has(response.status),
255
+ };
256
+ }
257
+
258
+ async #failureFromStatus(response: Response): Promise<JevFailureError> {
259
+ let detail = "";
260
+ try {
261
+ const parsed: unknown = await response.json();
262
+ const error = isRecord(parsed) ? parsed.error : undefined;
263
+ detail = errorDetail(error);
264
+ } catch {
265
+ detail = "";
266
+ }
267
+ const message = redactSecrets(
268
+ `Jev ${this.#transportLabel()} request failed with HTTP ${response.status}${detail ? `: ${detail}` : ""}.`
269
+ );
270
+ return new JevFailureError(statusCategory(response.status), message);
271
+ }
272
+
273
+ #transportLabel(): string {
274
+ return this.#endpoint === ENDPOINTS.openrouter ? "OpenRouter" : "TypeSafe";
275
+ }
276
+
277
+ async #parseSuccess(response: Response): Promise<AttemptOutcome> {
278
+ let parsed: unknown;
279
+ try {
280
+ parsed = await response.json();
281
+ } catch (error) {
282
+ return {
283
+ failure: new JevFailureError(
284
+ "malformed",
285
+ `Jev response was not JSON: ${redactSecrets(error instanceof Error ? error.message : String(error))}`
286
+ ),
287
+ };
288
+ }
289
+ if (!isRecord(parsed) || !parsed.answers || !isObjectLike(parsed.answers)) {
290
+ return {
291
+ failure: new JevFailureError(
292
+ "malformed",
293
+ "Jev response did not include an answers object."
294
+ ),
295
+ };
296
+ }
297
+ return {
298
+ // SAFETY: the response contract guarantees an answers object; the shape is re-validated per key by consumers.
299
+ answers: parsed.answers as RecordValue,
300
+ model: isString(parsed.model) ? parsed.model : this.#model,
301
+ usage: isRecordOf(parsed.usage)
302
+ ? {
303
+ input_tokens: parsed.usage.input_tokens,
304
+ output_tokens: parsed.usage.output_tokens,
305
+ }
306
+ : undefined,
307
+ };
308
+ }
309
+
310
+ #result(outcome: AttemptOutcome): JevAskResult {
311
+ const { usage } = outcome;
312
+ const inputTokens = finiteTokens(usage?.input_tokens);
313
+ const outputTokens = finiteTokens(usage?.output_tokens);
314
+ const price = this.#pricePerMtok ?? advisorJevPricePerMtokRef;
315
+ return {
316
+ answers: outcome.answers ?? {},
317
+ model: outcome.model ?? this.#model,
318
+ usage: {
319
+ cost: (inputTokens / 1_000_000) * price,
320
+ inputTokens,
321
+ outputTokens,
322
+ },
323
+ };
324
+ }
325
+ }
326
+
327
+ /** A client wired from resolved credentials; reads the live Jev settings per call. */
328
+ export const jevClientFromCredentials = (
329
+ credentials: JevCredentials,
330
+ fetch?: Fetch
331
+ ): JevClient =>
332
+ new JevClient({
333
+ apiKey: credentials.apiKey,
334
+ fetch,
335
+ model: advisorJevModelRef,
336
+ timeoutMs: advisorJevTimeoutMsRef,
337
+ transport: credentials.transport,
338
+ });
@@ -0,0 +1,19 @@
1
+ export type JevErrorCategory =
2
+ | "auth"
3
+ | "timeout"
4
+ | "network"
5
+ | "malformed"
6
+ | "error";
7
+
8
+ /** A classified, redacted Jev failure; never carries key material. */
9
+ export class JevFailureError extends Error {
10
+ readonly category: JevErrorCategory;
11
+
12
+ constructor(category: JevErrorCategory, message: string) {
13
+ super(message);
14
+ this.name = "JevFailureError";
15
+ this.category = category;
16
+ }
17
+ }
18
+
19
+ export { JevFailureError as JevFailure };