@parcha/agentrun-jev 0.1.0-beta.21 → 0.1.0-beta.23

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 (3) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +20 -13
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -77,7 +77,7 @@ const result = await runJudge({
77
77
  console.log(result.answers.team);
78
78
  ```
79
79
 
80
- Without a key, `createJevRunner()` throws `JevError` (code `configuration`: "Could not configure Jev. Supply apiKey or set TYPESAFE_API_KEY, and check transport options.") on that line, before any request is made. The same holds for `provider: "openrouter"` without `OPENROUTER_API_KEY`.
80
+ Without a key, or with an `apiKey` that is blank, `createJevRunner()` throws `JevError` (code `configuration`: "Could not configure Jev. Supply apiKey or set TYPESAFE_API_KEY, and check transport options.") on that line, before any request is made. The same holds for `provider: "openrouter"` without `OPENROUTER_API_KEY`.
81
81
 
82
82
  `judge`, `pick`, `sift`, `route`, semantic predicates, and judgment verification use the same `runJudge` adapter. Choice, Noul, and Score answers retain their raw probabilities. The core validates answer IDs, probability keys and normalization, score rubrics, and value ranges before consuming a result. A typed answer records the model's decision. It does not prove that a real-world claim is true.
83
83
 
@@ -115,7 +115,7 @@ Cost: when the response reports `usage.cost` (USD) it is `cost_usd`; otherwise a
115
115
 
116
116
  The official `@typesafe-ai/sdk` resolves `TYPESAFE_API_KEY` and `TYPESAFE_BASE_URL`. Explicit `apiKey` and `baseURL` options take precedence. An API root ends before `/v1/systemone`. Set `fetch` for a custom transport, or supply a `client` with a `systemOne(request, options)` method. A custom client must honor the request signal and disable its own retries; it owns its credentials and cannot be combined with transport options.
117
117
 
118
- The adapter retries transient HTTP 408, 429, 5xx responses and connection failures with bounded exponential delays. On a 429 or 503 with a `Retry-After` (seconds or an HTTP date), it waits the larger of that and its own delay, and gives up at once, with attempts left, when that wait would not end before the timeout. When the attempts end on a 429 after an earlier 408, 5xx or lost connection, the error reports the first of those failures (its HTTP `status`, or code `connection`), since that attempt may have been billed; an `http` error with status 429 means every attempt was refused. SDK retries are disabled. Validation failures and other HTTP errors do not retry. The deadline and either the runner or per-call signal interrupt both requests and backoff. Cancellation bounds when the adapter returns; an uncooperative custom client can still continue its own work.
118
+ The adapter retries transient HTTP 408, 429, 5xx responses and connection failures with bounded exponential delays. On a 429 or 503 with a `Retry-After` (seconds or an HTTP date), it waits the larger of that and its own delay, and gives up at once, with attempts left, when that wait would not end before the timeout. When the attempts end on a 429 after an earlier 408, 5xx or lost connection, the error reports the first of those failures (its HTTP `status`, or code `connection`), since that attempt may have been billed; an `http` error with status 429 means every attempt was refused. SDK retries are disabled. Validation failures and other HTTP errors do not retry. The deadline and either the runner or per-call signal interrupt both requests and backoff. A call the deadline ends is `timeout`, whether the runner's timer or the SDK's per-attempt timer fired first, and is not retried. Cancellation bounds when the adapter returns; an uncooperative custom client can still continue its own work.
119
119
 
120
120
  Request state has no local byte cap by default. Set `maxStateBytes` to a positive safe integer to reject serialized UTF-8 JSON state exceeding that host-owned limit before transport; omission or `null` disables this guard. This is a byte guard, not a token estimate. [Provider model context limits](https://docs.typesafe.ai/models.md) still apply. The adapter never truncates or splits state or questions.
121
121
 
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { setTimeout as sleep } from "node:timers/promises";
3
- import { APIConnectionError, APIError, Image, TypeSafeClient } from "@typesafe-ai/sdk";
3
+ import { APIConnectionError, APIError, APITimeoutError, Image, TypeSafeClient } from "@typesafe-ai/sdk";
4
4
  import { validateAnswers, SystemOneError, isSystemOneResponseReason } from "@parcha/agentrun-dsl";
5
5
  const keyVariable = { typesafe: "TYPESAFE_API_KEY", openrouter: "OPENROUTER_API_KEY" };
6
6
  const OPENROUTER_BASE_URL = "https://openrouter.ai/api";
@@ -101,12 +101,14 @@ export function createJevRunner(options = {}) {
101
101
  const maxAttempts = options.maxAttempts ?? 3;
102
102
  const retryBaseMs = options.retryBaseMs ?? 500;
103
103
  const retryMaxMs = options.retryMaxMs ?? 5_000;
104
+ // A JavaScript caller's `client: null` is no client, the same as an absent one.
105
+ const hostClient = options.client ?? undefined;
104
106
  configuration(timeoutMs === null || milliseconds(timeoutMs), `timeoutMs must be null or a positive integer of at most ${MAX_TIMER_MS}.`);
105
107
  configuration(maxStateBytes === null || (Number.isSafeInteger(maxStateBytes) && maxStateBytes > 0), "maxStateBytes must be null or a positive safe integer.");
106
- configuration(timeoutMs !== null || options.client !== undefined, "The TypeSafe SDK requires a finite timeout. For timeoutMs: null, supply a cancellation-only host client.");
108
+ configuration(timeoutMs !== null || hostClient !== undefined, "The TypeSafe SDK requires a finite timeout. For timeoutMs: null, supply a cancellation-only host client.");
107
109
  configuration(Number.isInteger(maxAttempts) && maxAttempts >= 1 && maxAttempts <= 10, "maxAttempts must be an integer from 1 to 10.");
108
110
  configuration(milliseconds(retryBaseMs, true) && milliseconds(retryMaxMs, true) && retryBaseMs <= retryMaxMs, "Retry delays must be nonnegative integers; retryBaseMs cannot exceed retryMaxMs.");
109
- configuration(!options.client || (options.apiKey === undefined && options.baseURL === undefined && options.fetch === undefined && options.provider === undefined), "Supply either client or transport options, not both.");
111
+ configuration(hostClient === undefined || (options.apiKey === undefined && options.baseURL === undefined && options.fetch === undefined && options.provider === undefined), "Supply either client or transport options, not both.");
110
112
  const provider = options.provider ?? "typesafe";
111
113
  configuration(provider === "typesafe" || provider === "openrouter", "provider must be typesafe or openrouter.");
112
114
  const pricing = options.pricing ? { ...options.pricing } : undefined;
@@ -117,10 +119,11 @@ export function createJevRunner(options = {}) {
117
119
  let client;
118
120
  try {
119
121
  const key = options.apiKey ?? (provider === "openrouter" ? process.env[keyVariable.openrouter] : undefined);
120
- // The SDK falls back to TYPESAFE_API_KEY when given no key: for OpenRouter that would send one provider's key to another.
121
- if (provider === "openrouter" && (typeof key !== "string" || key.trim().length === 0))
122
+ // A blank key is refused on either provider: the SDK would send it as an empty bearer token. Given no key, the SDK falls back to
123
+ // TYPESAFE_API_KEY, so for OpenRouter an absent key is refused too: that would send one provider's key to another.
124
+ if ((key !== undefined || provider === "openrouter") && (typeof key !== "string" || key.trim().length === 0))
122
125
  throw new Error("no key");
123
- client = options.client ?? new TypeSafeClient({
126
+ client = hostClient ?? new TypeSafeClient({
124
127
  apiKey: key, baseURL: options.baseURL ?? (provider === "openrouter" ? OPENROUTER_BASE_URL : undefined), fetch: options.fetch,
125
128
  defaultModel: model, retry: { maxRetries: 0 }, timeout: timeoutMs ?? undefined, logLevel: "off",
126
129
  // Bodies go as they always have: uncompressed JSON, which a host's own fetch, a gateway and OpenRouter all read.
@@ -150,24 +153,25 @@ export function createJevRunner(options = {}) {
150
153
  const invalid = (reason, message, stateBytes) => new JevError("invalid_request", message, 0, undefined, { reason,
151
154
  ...(typeof params.label === "string" ? { label: params.label.slice(0, 200) } : {}),
152
155
  ...(stateBytes === undefined || maxStateBytes === null ? {} : { stateBytes, maxStateBytes }) });
153
- const state = params.state ?? null;
156
+ const state = params.state;
154
157
  let serializedState;
155
158
  try {
156
- if (typeof state !== "string" && state !== null && typeof state !== "object")
159
+ // The SDK refuses a request with no state, so a null or missing state is refused here, before anything is sent.
160
+ if (typeof state !== "string" && (typeof state !== "object" || state === null))
157
161
  throw new Error();
158
162
  serializedState = JSON.stringify(state);
159
163
  if (typeof serializedState !== "string")
160
164
  throw new Error();
161
165
  }
162
166
  catch {
163
- throw invalid("invalid_state", "Jev state must be JSON-serializable text, an object, an array or null.");
167
+ throw invalid("invalid_state", "Jev state must be JSON-serializable text, an object or an array.");
164
168
  }
165
169
  const stateBytes = Buffer.byteLength(serializedState, "utf8");
166
170
  if (maxStateBytes !== null && stateBytes > maxStateBytes)
167
171
  throw invalid("state_too_large", `Jev assembled state is ${stateBytes} UTF-8 bytes; configured host limit is ${maxStateBytes} bytes. No request was sent.`, stateBytes);
168
172
  const callModel = params.model ?? model;
169
173
  if (typeof callModel !== "string" || callModel.trim().length === 0)
170
- throw invalid("invalid_questions", "model must be a nonempty string; no request was sent.");
174
+ throw invalid("unsupported_model", "model must be a nonempty string; no request was sent.");
171
175
  try {
172
176
  body = JSON.stringify({ state: JSON.parse(serializedState), model: callModel, questions: params.questions });
173
177
  request = JSON.parse(body);
@@ -184,7 +188,7 @@ export function createJevRunner(options = {}) {
184
188
  throw invalid("invalid_assets", "assets must map each reference to an image; no request was sent.");
185
189
  const references = Object.keys(given);
186
190
  const carriesImage = references.length > 0;
187
- if ((carriesImage || hasBox) && provider !== "typesafe" && options.client === undefined)
191
+ if ((carriesImage || hasBox) && provider !== "typesafe")
188
192
  throw invalid("unsupported_provider", "Images and Box questions are sent to TypeSafe's own API only; the OpenRouter path is not verified for them. No request was sent.");
189
193
  if (hasBox && !answersBox(callModel))
190
194
  throw invalid("unsupported_model", `A Box question needs a model that answers it, by name: ${BOX_MODEL} or later, or jev-preview; jev-latest is refused whatever version it points at. No request was sent.`);
@@ -226,7 +230,9 @@ export function createJevRunner(options = {}) {
226
230
  }), signal, abortError);
227
231
  }
228
232
  catch (error) {
229
- if (signal.aborted)
233
+ // Each attempt's timeout is the call's remaining budget, so the client's timer and the deadline end together: whichever fires first,
234
+ // the deadline ended the call. A timed-out request may have been billed, and its code is what says so.
235
+ if (signal.aborted || (timeoutMs !== null && error instanceof APITimeoutError))
230
236
  throw abortError();
231
237
  const status = error instanceof APIError ? error.status : undefined;
232
238
  const retryable = status === 408 || status === 429 || (status !== undefined && status >= 500 && status <= 599) || error instanceof APIConnectionError;
@@ -265,8 +271,9 @@ export function createJevRunner(options = {}) {
265
271
  if (!record(result))
266
272
  throw invalidResponse("response_shape", "Jev returned an invalid response.");
267
273
  const answers = result.answers;
274
+ // Checked against the questions as they were sent: a value JSON cannot carry (an undefined criterion) was never asked.
268
275
  try {
269
- validateAnswers(params.questions, answers);
276
+ validateAnswers(request.questions, answers);
270
277
  }
271
278
  catch (error) {
272
279
  throw invalidResponse(error instanceof SystemOneError && isSystemOneResponseReason(error.responseReason) ? error.responseReason : "answer_validation", "Jev returned invalid answers or probability distributions.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parcha/agentrun-jev",
3
- "version": "0.1.0-beta.21",
3
+ "version": "0.1.0-beta.23",
4
4
  "description": "Optional Jev system one decisions for AgentRun workflows",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -27,7 +27,7 @@
27
27
  "test": "node --test test/*.test.mjs"
28
28
  },
29
29
  "dependencies": {
30
- "@parcha/agentrun-dsl": "0.1.0-beta.21",
30
+ "@parcha/agentrun-dsl": "0.1.0-beta.23",
31
31
  "@typesafe-ai/sdk": "0.7.0"
32
32
  },
33
33
  "repository": {