@parcha/agentrun-jev 0.1.0-beta.20 → 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.
- package/README.md +56 -45
- package/dist/index.js +48 -36
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,10 +6,58 @@ Beta, on npm under the `beta` dist-tag (`npm view @parcha/agentrun-jev dist-tags
|
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
8
|
npm install @parcha/agentrun-dsl@beta @parcha/agentrun-jev@beta
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Offline tests
|
|
12
|
+
|
|
13
|
+
To exercise the real adapter with no key and no network request, inject a client. After installing the packages, save this as `offline-jev.mjs` and run `node offline-jev.mjs`:
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import assert from 'node:assert/strict';
|
|
17
|
+
import { createJevRunner } from '@parcha/agentrun-jev';
|
|
18
|
+
|
|
19
|
+
const runJudge = createJevRunner({
|
|
20
|
+
maxAttempts: 1,
|
|
21
|
+
client: {
|
|
22
|
+
async systemOne(request, options) {
|
|
23
|
+
options.signal?.throwIfAborted();
|
|
24
|
+
assert.deepEqual(Object.keys(request.questions), ['answersQuestion']);
|
|
25
|
+
return {
|
|
26
|
+
answers: { answersQuestion: { type: 'noul', noul: 0.95 } },
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const result = await runJudge({
|
|
33
|
+
label: 'screen-evidence',
|
|
34
|
+
kind: 'judge',
|
|
35
|
+
state: {
|
|
36
|
+
question: 'Can emergency documentation fixes bypass prior approval?',
|
|
37
|
+
passage: 'During an incident, the on-call engineer may merge a fix immediately; a second person reviews it the next working day.',
|
|
38
|
+
},
|
|
39
|
+
questions: {
|
|
40
|
+
answersQuestion: {
|
|
41
|
+
type: 'noul',
|
|
42
|
+
instructions: 'Does the passage contain a specific rule or exception answering the question? Retain conditional and contrary evidence; reject topic mentions and promotional claims.',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
assert.equal(result.answers.answersQuestion.noul, 0.95);
|
|
47
|
+
console.log(result.answers.answersQuestion);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
This prints `{ type: 'noul', noul: 0.95 }`. The answer is **scripted**; it does not evaluate the passage or adapt to prompt changes. Answer IDs must match the requested question IDs. Noul returns a probability, not a boolean. Reported model and token usage may be omitted; the adapter preserves missing usage/cost as unknown instead of inventing measurements.
|
|
51
|
+
|
|
52
|
+
## Call Jev
|
|
53
|
+
|
|
54
|
+
A live call needs a key. Set it, then run the sample below as `live-jev.mjs` (`node live-jev.mjs`). The samples that follow are written in TypeScript; run them with a TypeScript runner such as `npx tsx`, or drop the `type` imports and annotations to run them as `.mjs`.
|
|
55
|
+
|
|
56
|
+
```sh
|
|
9
57
|
export TYPESAFE_API_KEY="your-key"
|
|
10
58
|
```
|
|
11
59
|
|
|
12
|
-
```
|
|
60
|
+
```js
|
|
13
61
|
import { createJevRunner } from '@parcha/agentrun-jev';
|
|
14
62
|
|
|
15
63
|
const runJudge = createJevRunner();
|
|
@@ -29,11 +77,13 @@ const result = await runJudge({
|
|
|
29
77
|
console.log(result.answers.team);
|
|
30
78
|
```
|
|
31
79
|
|
|
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
|
+
|
|
32
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.
|
|
33
83
|
|
|
34
84
|
`System One` is TypeSafe's typed-question interface, exposed by the SDK as `systemOne`. A Noul answer is a truth probability from 0 to 1; the DSL converts probabilities of at least 0.5 to `true` and retains the probability beside that value. Choice and Score confidence comes from the response. Jev does not return a separate confidence for Noul; the DSL derives its own gate-strength value from the distance from 0.5, scaled to the range 0 to 1. These numbers are useful gate inputs, not calibrated guarantees.
|
|
35
85
|
|
|
36
|
-
The public adapter API
|
|
86
|
+
The public adapter API is `createJevRunner`, `jevProviderFromEnv`, `JevError`, `isJevResponseReason`, and the types `JevOptions`, `JevClient`, `JevProvider`, `JevErrorCode`, `JevResponseReason`, `JevResponseDiagnostic`, `JevRequestDiagnostic`, `JevImage`, `JevJudge`, `JevJudgeParams` and `JevJudgeResult`. The shared question, answer and distribution helpers live in `@parcha/agentrun-dsl`.
|
|
37
87
|
|
|
38
88
|
## Configuration
|
|
39
89
|
|
|
@@ -65,13 +115,13 @@ Cost: when the response reports `usage.cost` (USD) it is `cost_usd`; otherwise a
|
|
|
65
115
|
|
|
66
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.
|
|
67
117
|
|
|
68
|
-
The adapter retries transient HTTP 408, 429, 5xx responses and connection failures with bounded exponential delays. 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.
|
|
69
119
|
|
|
70
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.
|
|
71
121
|
|
|
72
122
|
The core allows up to 240 options per choice and 2–10 levels per score rubric. Adapter timeouts are positive integer milliseconds up to 2,147,483,647; retry delays may also be zero. `maxAttempts` is 1–10, with a default of 3.
|
|
73
123
|
|
|
74
|
-
`JevError` provides a stable `code`, `attempts`, and optional HTTP `status`. Errors deliberately omit raw service bodies, headers, input state, and causes. SDK logging is disabled. Custom clients and transports own their own logging. Do not expose credentials in browser code.
|
|
124
|
+
`JevError` provides a stable `code`, `attempts`, and optional HTTP `status`. A request the runner refuses before sending it has code `invalid_request`, `attempts` 0 and a `requestDiagnostic` (`JevRequestDiagnostic`): a `reason` (`invalid_state`, `state_too_large`, `invalid_questions`, `invalid_assets`, `unsupported_model` or `unsupported_provider`), the call's `label` (at most 200 characters) when it has one, and for a state over `maxStateBytes` its `stateBytes` and `maxStateBytes`. Errors deliberately omit raw service bodies, headers, input state, and causes. SDK logging is disabled. Custom clients and transports own their own logging. Do not expose credentials in browser code.
|
|
75
125
|
|
|
76
126
|
Optional `responseDiagnostic.reason` identifies a fixed response invariant, such
|
|
77
127
|
as `answer_keys`, `probability_mass`, or `token_usage`. An explicitly structured
|
|
@@ -79,7 +129,7 @@ capacity rejection can report `max_tokens_exceeded`; arbitrary provider messages
|
|
|
79
129
|
are never parsed for a reason. These details do not change validation, retry
|
|
80
130
|
policy, or the submitted evidence. Unknown causes remain unspecified.
|
|
81
131
|
|
|
82
|
-
Successful results retain `answers`, reported `model`, reported token `usage`, and a SHA-256 of the submitted request. `cost_usd` is `
|
|
132
|
+
Successful results retain `answers`, reported `model`, reported token `usage`, and a SHA-256 of the submitted request. `cost_usd` is the response's reported `usage.cost` when it has one, else an estimate from the counted tokens and an explicit `pricing: { inputUsdPerMillionTokens, outputUsdPerMillionTokens }`, else `null`. Configured prices produce an estimate for the successful response; costs from unsuccessful or interrupted attempts are unknown.
|
|
83
133
|
|
|
84
134
|
## Images and Box questions
|
|
85
135
|
|
|
@@ -111,46 +161,7 @@ const { answers, usage, cost_usd } = await judge({
|
|
|
111
161
|
- Request bodies are sent uncompressed; the SDK's default compresses them with zstd.
|
|
112
162
|
- Box is a preview: Jev's page says its request format, answers and limits can change between releases.
|
|
113
163
|
|
|
114
|
-
##
|
|
115
|
-
|
|
116
|
-
Inject a client to exercise the real adapter without a key or network request. After installing the DSL and Jev packages, save this as `offline-jev.mjs` and run `node offline-jev.mjs`:
|
|
117
|
-
|
|
118
|
-
```js
|
|
119
|
-
import assert from 'node:assert/strict';
|
|
120
|
-
import { createJevRunner } from '@parcha/agentrun-jev';
|
|
121
|
-
|
|
122
|
-
const runJudge = createJevRunner({
|
|
123
|
-
maxAttempts: 1,
|
|
124
|
-
client: {
|
|
125
|
-
async systemOne(request, options) {
|
|
126
|
-
options.signal?.throwIfAborted();
|
|
127
|
-
assert.deepEqual(Object.keys(request.questions), ['answersQuestion']);
|
|
128
|
-
return {
|
|
129
|
-
answers: { answersQuestion: { type: 'noul', noul: 0.95 } },
|
|
130
|
-
};
|
|
131
|
-
},
|
|
132
|
-
},
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
const result = await runJudge({
|
|
136
|
-
label: 'screen-evidence',
|
|
137
|
-
kind: 'judge',
|
|
138
|
-
state: {
|
|
139
|
-
question: 'Can emergency documentation fixes bypass prior approval?',
|
|
140
|
-
passage: 'During an incident, the on-call engineer may merge a fix immediately; a second person reviews it the next working day.',
|
|
141
|
-
},
|
|
142
|
-
questions: {
|
|
143
|
-
answersQuestion: {
|
|
144
|
-
type: 'noul',
|
|
145
|
-
instructions: 'Does the passage contain a specific rule or exception answering the question? Retain conditional and contrary evidence; reject topic mentions and promotional claims.',
|
|
146
|
-
},
|
|
147
|
-
},
|
|
148
|
-
});
|
|
149
|
-
assert.equal(result.answers.answersQuestion.noul, 0.95);
|
|
150
|
-
console.log(result.answers.answersQuestion);
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
This prints `{ type: 'noul', noul: 0.95 }`. The answer is **scripted**; it does not evaluate the passage or adapt to prompt changes. Answer IDs must match the requested question IDs. Noul returns a probability, not a boolean. Reported model and token usage may be omitted; the adapter preserves missing usage/cost as unknown instead of inventing measurements.
|
|
164
|
+
## Testing your own host
|
|
154
165
|
|
|
155
166
|
The [standalone TypeScript starter](https://github.com/Parcha-ai/agentrun/tree/main/examples/starter#readme) includes npm installation, a complete search-and-screen workflow, and tests for missing evidence and the screening step alone. Its injected client answers every named question generated by `sift`.
|
|
156
167
|
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
3
|
+
import { APIConnectionError, APIError, APITimeoutError, Image, TypeSafeClient } from "@typesafe-ai/sdk";
|
|
3
4
|
import { validateAnswers, SystemOneError, isSystemOneResponseReason } from "@parcha/agentrun-dsl";
|
|
4
5
|
const keyVariable = { typesafe: "TYPESAFE_API_KEY", openrouter: "OPENROUTER_API_KEY" };
|
|
5
6
|
const OPENROUTER_BASE_URL = "https://openrouter.ai/api";
|
|
@@ -7,10 +8,13 @@ const OPENROUTER_DEFAULT_MODEL = "~typesafe/jev-latest";
|
|
|
7
8
|
/** The provider an environment's keys select: OpenRouter only when `OPENROUTER_API_KEY` is set and `TYPESAFE_API_KEY` is not. The runner itself never reads this. */
|
|
8
9
|
export function jevProviderFromEnv(env = process.env) {
|
|
9
10
|
const set = (name) => typeof env[name] === "string" && env[name].trim().length > 0;
|
|
10
|
-
return set(
|
|
11
|
+
return set(keyVariable.openrouter) && !set(keyVariable.typesafe) ? "openrouter" : "typesafe";
|
|
11
12
|
}
|
|
12
13
|
const IMAGE_HEADER = "TypeSafe-Danger-Zone-Images";
|
|
13
14
|
const MAX_IMAGE_BYTES = 16 * 1024 * 1024;
|
|
15
|
+
const MAX_REFERENCE_BYTES = 1024;
|
|
16
|
+
/** The longest delay a Node timer accepts. */
|
|
17
|
+
const MAX_TIMER_MS = 2_147_483_647;
|
|
14
18
|
const BOX_MODEL = "jev-1.14.0";
|
|
15
19
|
/** Models that answer a Box question: Jev 1.14 and later by name, and the preview alias. The check is by name, so `jev-latest` is refused whatever version it points at. */
|
|
16
20
|
function answersBox(model) {
|
|
@@ -58,7 +62,7 @@ const configuration = (condition, message) => {
|
|
|
58
62
|
if (!condition)
|
|
59
63
|
throw new JevError("configuration", message);
|
|
60
64
|
};
|
|
61
|
-
const milliseconds = (n, allowZero = false) => Number.isSafeInteger(n) && n >= (allowZero ? 0 : 1) && n <=
|
|
65
|
+
const milliseconds = (n, allowZero = false) => Number.isSafeInteger(n) && n >= (allowZero ? 0 : 1) && n <= MAX_TIMER_MS;
|
|
62
66
|
const record = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
63
67
|
const tokens = (n) => typeof n === "number" && Number.isSafeInteger(n) && n >= 0;
|
|
64
68
|
function abortable(promise, signal, error) {
|
|
@@ -73,14 +77,13 @@ function abortable(promise, signal, error) {
|
|
|
73
77
|
abort();
|
|
74
78
|
});
|
|
75
79
|
}
|
|
76
|
-
function wait(ms, signal, error) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
});
|
|
80
|
+
async function wait(ms, signal, error) {
|
|
81
|
+
try {
|
|
82
|
+
await sleep(ms, undefined, { signal });
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
throw error();
|
|
86
|
+
}
|
|
84
87
|
}
|
|
85
88
|
/** A server's Retry-After in milliseconds from now, given as delta-seconds or an HTTP date; undefined when absent or unparseable. */
|
|
86
89
|
function retryAfterMs(headers) {
|
|
@@ -98,12 +101,14 @@ export function createJevRunner(options = {}) {
|
|
|
98
101
|
const maxAttempts = options.maxAttempts ?? 3;
|
|
99
102
|
const retryBaseMs = options.retryBaseMs ?? 500;
|
|
100
103
|
const retryMaxMs = options.retryMaxMs ?? 5_000;
|
|
101
|
-
|
|
104
|
+
// A JavaScript caller's `client: null` is no client, the same as an absent one.
|
|
105
|
+
const hostClient = options.client ?? undefined;
|
|
106
|
+
configuration(timeoutMs === null || milliseconds(timeoutMs), `timeoutMs must be null or a positive integer of at most ${MAX_TIMER_MS}.`);
|
|
102
107
|
configuration(maxStateBytes === null || (Number.isSafeInteger(maxStateBytes) && maxStateBytes > 0), "maxStateBytes must be null or a positive safe integer.");
|
|
103
|
-
configuration(timeoutMs !== null ||
|
|
108
|
+
configuration(timeoutMs !== null || hostClient !== undefined, "The TypeSafe SDK requires a finite timeout. For timeoutMs: null, supply a cancellation-only host client.");
|
|
104
109
|
configuration(Number.isInteger(maxAttempts) && maxAttempts >= 1 && maxAttempts <= 10, "maxAttempts must be an integer from 1 to 10.");
|
|
105
110
|
configuration(milliseconds(retryBaseMs, true) && milliseconds(retryMaxMs, true) && retryBaseMs <= retryMaxMs, "Retry delays must be nonnegative integers; retryBaseMs cannot exceed retryMaxMs.");
|
|
106
|
-
configuration(
|
|
111
|
+
configuration(hostClient === undefined || (options.apiKey === undefined && options.baseURL === undefined && options.fetch === undefined && options.provider === undefined), "Supply either client or transport options, not both.");
|
|
107
112
|
const provider = options.provider ?? "typesafe";
|
|
108
113
|
configuration(provider === "typesafe" || provider === "openrouter", "provider must be typesafe or openrouter.");
|
|
109
114
|
const pricing = options.pricing ? { ...options.pricing } : undefined;
|
|
@@ -113,11 +118,12 @@ export function createJevRunner(options = {}) {
|
|
|
113
118
|
configuration(typeof model === "string" && model.trim().length > 0, "model must be a nonempty string.");
|
|
114
119
|
let client;
|
|
115
120
|
try {
|
|
116
|
-
const key = options.apiKey ?? (provider === "openrouter" ? process.env.
|
|
117
|
-
//
|
|
118
|
-
|
|
121
|
+
const key = options.apiKey ?? (provider === "openrouter" ? process.env[keyVariable.openrouter] : undefined);
|
|
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))
|
|
119
125
|
throw new Error("no key");
|
|
120
|
-
client =
|
|
126
|
+
client = hostClient ?? new TypeSafeClient({
|
|
121
127
|
apiKey: key, baseURL: options.baseURL ?? (provider === "openrouter" ? OPENROUTER_BASE_URL : undefined), fetch: options.fetch,
|
|
122
128
|
defaultModel: model, retry: { maxRetries: 0 }, timeout: timeoutMs ?? undefined, logLevel: "off",
|
|
123
129
|
// Bodies go as they always have: uncompressed JSON, which a host's own fetch, a gateway and OpenRouter all read.
|
|
@@ -136,6 +142,7 @@ export function createJevRunner(options = {}) {
|
|
|
136
142
|
const abortError = () => (runnerSignal?.aborted || params.signal?.aborted)
|
|
137
143
|
? new JevError("aborted", "Jev request was cancelled.", attempt)
|
|
138
144
|
: new JevError("timeout", "Jev request exceeded its total deadline.", attempt);
|
|
145
|
+
const invalidResponse = (reason, message) => new JevError("invalid_response", message, attempt, undefined, undefined, { reason });
|
|
139
146
|
if (signal.aborted)
|
|
140
147
|
throw abortError();
|
|
141
148
|
const timer = timeoutMs === null ? undefined : setTimeout(() => deadline.abort(), timeoutMs);
|
|
@@ -146,24 +153,25 @@ export function createJevRunner(options = {}) {
|
|
|
146
153
|
const invalid = (reason, message, stateBytes) => new JevError("invalid_request", message, 0, undefined, { reason,
|
|
147
154
|
...(typeof params.label === "string" ? { label: params.label.slice(0, 200) } : {}),
|
|
148
155
|
...(stateBytes === undefined || maxStateBytes === null ? {} : { stateBytes, maxStateBytes }) });
|
|
149
|
-
const state = params.state
|
|
156
|
+
const state = params.state;
|
|
150
157
|
let serializedState;
|
|
151
158
|
try {
|
|
152
|
-
|
|
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))
|
|
153
161
|
throw new Error();
|
|
154
162
|
serializedState = JSON.stringify(state);
|
|
155
163
|
if (typeof serializedState !== "string")
|
|
156
164
|
throw new Error();
|
|
157
165
|
}
|
|
158
166
|
catch {
|
|
159
|
-
throw invalid("invalid_state", "Jev state must be JSON-serializable text, an object
|
|
167
|
+
throw invalid("invalid_state", "Jev state must be JSON-serializable text, an object or an array.");
|
|
160
168
|
}
|
|
161
169
|
const stateBytes = Buffer.byteLength(serializedState, "utf8");
|
|
162
170
|
if (maxStateBytes !== null && stateBytes > maxStateBytes)
|
|
163
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);
|
|
164
172
|
const callModel = params.model ?? model;
|
|
165
173
|
if (typeof callModel !== "string" || callModel.trim().length === 0)
|
|
166
|
-
throw invalid("
|
|
174
|
+
throw invalid("unsupported_model", "model must be a nonempty string; no request was sent.");
|
|
167
175
|
try {
|
|
168
176
|
body = JSON.stringify({ state: JSON.parse(serializedState), model: callModel, questions: params.questions });
|
|
169
177
|
request = JSON.parse(body);
|
|
@@ -175,12 +183,12 @@ export function createJevRunner(options = {}) {
|
|
|
175
183
|
}
|
|
176
184
|
// Images and Box questions: validated whole before any send; the header goes with exactly these requests.
|
|
177
185
|
const hasBox = Object.values(request.questions).some(question => record(question) && question.type === "box");
|
|
178
|
-
const given = params.assets
|
|
186
|
+
const given = params.assets ?? {};
|
|
179
187
|
if (!record(given))
|
|
180
188
|
throw invalid("invalid_assets", "assets must map each reference to an image; no request was sent.");
|
|
181
189
|
const references = Object.keys(given);
|
|
182
190
|
const carriesImage = references.length > 0;
|
|
183
|
-
if ((carriesImage || hasBox) && provider !== "typesafe"
|
|
191
|
+
if ((carriesImage || hasBox) && provider !== "typesafe")
|
|
184
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.");
|
|
185
193
|
if (hasBox && !answersBox(callModel))
|
|
186
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.`);
|
|
@@ -195,11 +203,11 @@ export function createJevRunner(options = {}) {
|
|
|
195
203
|
assetDigests = Object.create(null);
|
|
196
204
|
for (const reference of references) {
|
|
197
205
|
const image = given[reference];
|
|
198
|
-
const sound =
|
|
206
|
+
const sound = reference.length > 0 && Buffer.byteLength(reference, "utf8") <= MAX_REFERENCE_BYTES && !/["\\\u0000-\u001f]/.test(reference)
|
|
199
207
|
&& record(image) && image.bytes instanceof Uint8Array && (image.mime === "image/png" || image.mime === "image/jpeg")
|
|
200
208
|
&& image.bytes.byteLength > 0 && image.bytes.byteLength <= MAX_IMAGE_BYTES && startsWith(image.bytes, imageMagic[image.mime]);
|
|
201
209
|
if (!sound)
|
|
202
|
-
throw invalid("invalid_assets",
|
|
210
|
+
throw invalid("invalid_assets", `Each image is a PNG or JPEG file of at most ${MAX_IMAGE_BYTES / 1024 / 1024} MiB, with bytes that are that type and a plain reference. No request was sent.`);
|
|
203
211
|
if (!texts.some(text => text.includes(reference)))
|
|
204
212
|
throw invalid("invalid_assets", "Every image reference must appear in the state or a question. No request was sent.");
|
|
205
213
|
assets[reference] = new Image(image.bytes);
|
|
@@ -222,7 +230,9 @@ export function createJevRunner(options = {}) {
|
|
|
222
230
|
}), signal, abortError);
|
|
223
231
|
}
|
|
224
232
|
catch (error) {
|
|
225
|
-
|
|
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))
|
|
226
236
|
throw abortError();
|
|
227
237
|
const status = error instanceof APIError ? error.status : undefined;
|
|
228
238
|
const retryable = status === 408 || status === 429 || (status !== undefined && status >= 500 && status <= 599) || error instanceof APIConnectionError;
|
|
@@ -232,9 +242,10 @@ export function createJevRunner(options = {}) {
|
|
|
232
242
|
const backoff = Math.min(retryMaxMs, retryBaseMs * 2 ** (attempt - 1));
|
|
233
243
|
const asked = error instanceof APIError && (status === 429 || status === 503) ? retryAfterMs(error.headers) : undefined;
|
|
234
244
|
const delay = asked === undefined ? backoff : Math.max(asked, backoff);
|
|
235
|
-
// A server's wait is kept in full or the loop gives up now:
|
|
236
|
-
//
|
|
237
|
-
|
|
245
|
+
// A server's Retry-After wait is kept in full or the loop gives up now: a wait that would not end before the deadline (or within the longest
|
|
246
|
+
// timer, when there is none) is never started, since the retry after it would have no time left to answer (a request that times out has a
|
|
247
|
+
// cost no one knows). A backoff wait is not checked here: the deadline ends it.
|
|
248
|
+
const left = timeoutMs === null ? MAX_TIMER_MS : timeoutMs - (Date.now() - started);
|
|
238
249
|
if (asked === undefined || delay < left) {
|
|
239
250
|
await wait(delay, signal, abortError);
|
|
240
251
|
continue;
|
|
@@ -258,13 +269,14 @@ export function createJevRunner(options = {}) {
|
|
|
258
269
|
throw new JevError("invalid_request", "Jev could not process the request. Check question and transport configuration.", attempt);
|
|
259
270
|
}
|
|
260
271
|
if (!record(result))
|
|
261
|
-
throw
|
|
272
|
+
throw invalidResponse("response_shape", "Jev returned an invalid response.");
|
|
262
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.
|
|
263
275
|
try {
|
|
264
|
-
validateAnswers(
|
|
276
|
+
validateAnswers(request.questions, answers);
|
|
265
277
|
}
|
|
266
278
|
catch (error) {
|
|
267
|
-
throw
|
|
279
|
+
throw invalidResponse(error instanceof SystemOneError && isSystemOneResponseReason(error.responseReason) ? error.responseReason : "answer_validation", "Jev returned invalid answers or probability distributions.");
|
|
268
280
|
}
|
|
269
281
|
const usage = result.usage;
|
|
270
282
|
// Usage carries token counts, a provider-reported cost (USD), or both; a half pair of counts or a bad cost is invalid.
|
|
@@ -273,16 +285,16 @@ export function createJevRunner(options = {}) {
|
|
|
273
285
|
const reportedCost = reported && typeof usage.cost === "number" && Number.isFinite(usage.cost) && usage.cost >= 0 ? usage.cost : undefined;
|
|
274
286
|
const noCounts = record(usage) && usage.input_tokens === undefined && usage.output_tokens === undefined;
|
|
275
287
|
if (usage != null && (!record(usage) || (reported ? reportedCost === undefined || !(counted || noCounts) : !counted))) {
|
|
276
|
-
throw
|
|
288
|
+
throw invalidResponse("token_usage", "Jev returned invalid token usage.");
|
|
277
289
|
}
|
|
278
290
|
if (result.model != null && (typeof result.model !== "string" || !result.model.trim())) {
|
|
279
|
-
throw
|
|
291
|
+
throw invalidResponse("model_identifier", "Jev returned an invalid model identifier.");
|
|
280
292
|
}
|
|
281
293
|
const metering = counted ? { input_tokens: usage.input_tokens, output_tokens: usage.output_tokens } : null;
|
|
282
294
|
// The provider's own cost wins; else the configured estimate; else unknown (null), never 0.
|
|
283
295
|
const cost = reportedCost !== undefined ? reportedCost : pricing && metering ? (metering.input_tokens * pricing.inputUsdPerMillionTokens + metering.output_tokens * pricing.outputUsdPerMillionTokens) / 1_000_000 : null;
|
|
284
296
|
if (cost !== null && !Number.isFinite(cost))
|
|
285
|
-
throw
|
|
297
|
+
throw invalidResponse("cost_range", "Jev cost estimate exceeded the numeric range.");
|
|
286
298
|
return {
|
|
287
299
|
answers, model: typeof result.model === "string" ? result.model : null,
|
|
288
300
|
usage: metering, cost_usd: cost,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parcha/agentrun-jev",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
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.
|
|
30
|
+
"@parcha/agentrun-dsl": "0.1.0-beta.23",
|
|
31
31
|
"@typesafe-ai/sdk": "0.7.0"
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|