runline 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/plugins/_shared/authedFetch.js +33 -0
- package/dist/plugins/_shared/provider.js +80 -0
- package/dist/plugins/_shared/tokenRefresh.js +5 -3
- package/dist/plugins/gett/src/index.js +358 -0
- package/dist/plugins/gett/src/places.js +140 -0
- package/dist/plugins/gett/src/rides.js +144 -0
- package/dist/plugins/gett/src/session.js +242 -0
- package/dist/plugins/gett/src/shared.js +180 -0
- package/dist/plugins/linear/src/attachments.js +2 -2
- package/dist/plugins/linear/src/cycles.js +6 -6
- package/dist/plugins/linear/src/index.js +2 -2
- package/dist/plugins/linear/src/initiatives.js +6 -6
- package/dist/plugins/linear/src/issues.js +9 -9
- package/dist/plugins/linear/src/labels.js +6 -6
- package/dist/plugins/linear/src/organization.js +2 -2
- package/dist/plugins/linear/src/projects.js +12 -12
- package/dist/plugins/linear/src/shared.js +148 -20
- package/dist/plugins/linear/src/states.js +3 -3
- package/dist/plugins/linear/src/teams.js +6 -5
- package/dist/plugins/linear/src/users.js +3 -3
- package/dist/plugins/linear/src/views.js +10 -8
- package/dist/plugins/linear/src/webhooks.js +5 -5
- package/dist/plugins/openai/src/index.js +28 -9
- package/dist/plugins/typesafe/src/guide.js +172 -0
- package/dist/plugins/typesafe/src/index.js +169 -0
- package/dist/plugins/typesafe/src/questions.js +180 -0
- package/dist/plugins/typesafe/src/shared.js +166 -0
- package/dist/plugins/wolt/src/account.js +427 -0
- package/dist/plugins/wolt/src/catalog.js +166 -0
- package/dist/plugins/wolt/src/index.js +526 -0
- package/dist/plugins/wolt/src/orders.js +349 -0
- package/dist/plugins/wolt/src/shared.js +218 -0
- package/package.json +1 -1
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import * as t from "typebox";
|
|
2
|
+
import { STRICT } from "./shared.js";
|
|
3
|
+
/**
|
|
4
|
+
* The three question primitives, as schemas an agent can read through
|
|
5
|
+
* `actions.describe` and as the wire shape the API expects.
|
|
6
|
+
*
|
|
7
|
+
* The wire format keys questions by an id in a map and calls the option
|
|
8
|
+
* rubric `criteria` in all three types, which describe() cannot walk into
|
|
9
|
+
* (a free-form map has no declared properties). So the plugin takes an
|
|
10
|
+
* array of typed questions carrying their own id, and each primitive names
|
|
11
|
+
* its rubric after what that rubric actually is — `options`, `levels`,
|
|
12
|
+
* `whenTrue`/`whenFalse`. Conversion to the wire shape happens here.
|
|
13
|
+
* `typesafe.raw` remains available for the literal API body.
|
|
14
|
+
*/
|
|
15
|
+
const ATOMIC = "One narrow, coherent judgment, carrying its full meaning on its own — the question id is never sent to the model. " +
|
|
16
|
+
"Jev is literal: it answers the words you wrote, not the intent behind them, and scoping words, negations, and implied " +
|
|
17
|
+
"conditions are read at face value. State the exact condition, and reference nested state by backticked path, e.g. " +
|
|
18
|
+
"`ticket.messages[0].text`. Split independently useful dimensions into separate questions and combine them in code — " +
|
|
19
|
+
"but do not split apart a relationship you are asking about, and do not mistake atomic for trivial: a bounded action " +
|
|
20
|
+
"selection or a contextual interpretation is one judgment, not several. A plain string carries a simple question; pass " +
|
|
21
|
+
"an object or array when definitions, contrasts, exclusions, or examples make the judgment clearer.";
|
|
22
|
+
const idSchema = t.String({
|
|
23
|
+
minLength: 1,
|
|
24
|
+
maxLength: 128,
|
|
25
|
+
pattern: "\\S",
|
|
26
|
+
description: "Your key for this question. The answer comes back under the same key. Never sent to the model, so it costs nothing and does not influence inference — name it for your code.",
|
|
27
|
+
});
|
|
28
|
+
/**
|
|
29
|
+
* Instructions carry structure as readily as prose. The branches stay
|
|
30
|
+
* description-free, so the guidance lives once on the union and no branch
|
|
31
|
+
* can contradict it as the shapes drift apart.
|
|
32
|
+
*/
|
|
33
|
+
const instructionsSchema = t.Union([
|
|
34
|
+
t.String({ minLength: 1, maxLength: 8_000 }),
|
|
35
|
+
t.Record(t.String(), t.Unknown()),
|
|
36
|
+
t.Array(t.Unknown()),
|
|
37
|
+
], { description: ATOMIC });
|
|
38
|
+
/**
|
|
39
|
+
* Each primitive's rubric, shared by two surfaces: the question objects
|
|
40
|
+
* `typesafe.evaluate` takes, and the flat input of the single-question
|
|
41
|
+
* shorthands, which carry no id and imply their own type.
|
|
42
|
+
*/
|
|
43
|
+
export const choiceRubric = {
|
|
44
|
+
instructions: instructionsSchema,
|
|
45
|
+
options: t.Array(t.Object({
|
|
46
|
+
value: t.String({
|
|
47
|
+
minLength: 1,
|
|
48
|
+
maxLength: 256,
|
|
49
|
+
description: "The option your code branches on. Returned verbatim as `choice` and as a key in `probabilities`.",
|
|
50
|
+
}),
|
|
51
|
+
description: t.Optional(t.String({
|
|
52
|
+
maxLength: 2_000,
|
|
53
|
+
description: "What this option means, and which boundary cases belong in it. Treat it as an extension of the instruction: options that argue against the instruction, or overlap each other, lose accuracy.",
|
|
54
|
+
})),
|
|
55
|
+
}, STRICT), {
|
|
56
|
+
minItems: 2,
|
|
57
|
+
maxItems: 255,
|
|
58
|
+
description: "The closed set of answers, mutually exclusive and jointly covering the state. Include a no-match outcome when " +
|
|
59
|
+
"nothing may fit — an escape hatch the model can choose beats inferring one from low confidence — or ask a " +
|
|
60
|
+
"separate presence Noul when that check is useful on its own. If several labels can be true at once, this is not " +
|
|
61
|
+
"a Choice: ask one Noul per label. When the options are values pulled from a source, check candidate coverage " +
|
|
62
|
+
"first — the model cannot choose a value you did not include. Cardinality is capped at 255; past roughly a dozen " +
|
|
63
|
+
"options the model runs a slower two-stage score-then-choose pass, so latency climbs.",
|
|
64
|
+
}),
|
|
65
|
+
};
|
|
66
|
+
export const scoreRubric = {
|
|
67
|
+
instructions: instructionsSchema,
|
|
68
|
+
levels: t.Array(t.String({
|
|
69
|
+
minLength: 1,
|
|
70
|
+
maxLength: 2_000,
|
|
71
|
+
description: "A concrete situation this level describes, standing on its own without reference to the neighbouring levels — " +
|
|
72
|
+
"e.g. 'Calm, just stating facts'. Index 0 is the low end.",
|
|
73
|
+
}), {
|
|
74
|
+
minItems: 2,
|
|
75
|
+
maxItems: 20,
|
|
76
|
+
description: "Ordered level descriptions, ascending, at least two. The returned score is the probability-weighted position " +
|
|
77
|
+
"across them, so 1.6 means the mass sits between levels 1 and 2. Threshold on that value freely, but do not " +
|
|
78
|
+
"interpolate a real-world magnitude out of it — level calibration is weak numerically. " +
|
|
79
|
+
"Levels can also be the actions you will take (merge / leave / escalate), which removes the threshold entirely.",
|
|
80
|
+
}),
|
|
81
|
+
};
|
|
82
|
+
export const noulRubric = {
|
|
83
|
+
instructions: instructionsSchema,
|
|
84
|
+
whenTrue: t.Optional(t.String({
|
|
85
|
+
maxLength: 2_000,
|
|
86
|
+
description: "What a yes (value near 1) means. Keep it aligned with the instruction: a noul whose true maps to 'no' performs measurably worse.",
|
|
87
|
+
})),
|
|
88
|
+
whenFalse: t.Optional(t.String({
|
|
89
|
+
maxLength: 2_000,
|
|
90
|
+
description: "What a no (value near 0) means.",
|
|
91
|
+
})),
|
|
92
|
+
};
|
|
93
|
+
const choiceQuestionSchema = t.Object({
|
|
94
|
+
id: idSchema,
|
|
95
|
+
type: t.Literal("choice", {
|
|
96
|
+
description: "Pick exactly one option. Answer: choice (highest-probability option), probabilities (every option, summing to 1), " +
|
|
97
|
+
"confidence. The distribution weighs the options you declared against each other, not against anything outside the set.",
|
|
98
|
+
}),
|
|
99
|
+
...choiceRubric,
|
|
100
|
+
}, STRICT);
|
|
101
|
+
const scoreQuestionSchema = t.Object({
|
|
102
|
+
id: idSchema,
|
|
103
|
+
type: t.Literal("score", {
|
|
104
|
+
description: "Rate the state along an ordered rubric. Answer: score (probability-weighted, may land between levels), legend, " +
|
|
105
|
+
"probabilities per level, confidence. Comparable per-item Scores are how you rank a list by degree.",
|
|
106
|
+
}),
|
|
107
|
+
...scoreRubric,
|
|
108
|
+
}, STRICT);
|
|
109
|
+
const noulQuestionSchema = t.Object({
|
|
110
|
+
id: idSchema,
|
|
111
|
+
type: t.Literal("noul", {
|
|
112
|
+
description: "Is this statement true? Answer: noul, a probability from 0 (no) to 1 (yes), with no separate confidence field — " +
|
|
113
|
+
"the probability is the uncertainty. 0.5 means yes and no are equally likely; it is not a medium-intensity " +
|
|
114
|
+
"reading, so use a Score when you want degree. One Noul per label is the right shape when several labels may apply.",
|
|
115
|
+
}),
|
|
116
|
+
...noulRubric,
|
|
117
|
+
}, STRICT);
|
|
118
|
+
const questionSchema = t.Union([choiceQuestionSchema, scoreQuestionSchema, noulQuestionSchema], {
|
|
119
|
+
description: "One typed question: choice (pick one), score (rate on a rubric), or noul (is this true). " +
|
|
120
|
+
"Set `type` and supply that type's rubric — options, levels, or whenTrue/whenFalse.",
|
|
121
|
+
});
|
|
122
|
+
export const questionsSchema = t.Array(questionSchema, {
|
|
123
|
+
minItems: 1,
|
|
124
|
+
maxItems: 255,
|
|
125
|
+
description: "Every question to ask about this state. They are evaluated in parallel and in isolation against the same state in one " +
|
|
126
|
+
"pass, and cannot see each other's answers, so ten questions cost barely more wall-clock time than one and no question " +
|
|
127
|
+
"pollutes another's context. Batch every question you might need, including speculative ones — but state each " +
|
|
128
|
+
"speculative premise explicitly in its own instructions, and note that extra questions are near-free in latency, not " +
|
|
129
|
+
"in tokens: every question's text is billed as input. A second request is the right call when an answer here decides " +
|
|
130
|
+
"what evidence to fetch, what state to build, or what the next options are. Types mix freely. Ids must be unique.",
|
|
131
|
+
});
|
|
132
|
+
/**
|
|
133
|
+
* Ids and option values are caller-chosen strings, so every map keyed by
|
|
134
|
+
* them is built through a Map and materialized with `Object.fromEntries`.
|
|
135
|
+
* A plain object would report `toString` as a duplicate it never saw and
|
|
136
|
+
* would drop `__proto__` on assignment, silently shrinking the request.
|
|
137
|
+
*/
|
|
138
|
+
export function toWireQuestion(question) {
|
|
139
|
+
if (question.type === "choice") {
|
|
140
|
+
const criteria = new Map();
|
|
141
|
+
for (const option of question.options) {
|
|
142
|
+
if (criteria.has(option.value)) {
|
|
143
|
+
throw new Error(`Question "${question.id}" repeats the option "${option.value}".`);
|
|
144
|
+
}
|
|
145
|
+
criteria.set(option.value, option.description ?? null);
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
type: "choice",
|
|
149
|
+
instructions: question.instructions,
|
|
150
|
+
criteria: Object.fromEntries(criteria),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
if (question.type === "score") {
|
|
154
|
+
return {
|
|
155
|
+
type: "score",
|
|
156
|
+
instructions: question.instructions,
|
|
157
|
+
criteria: question.levels,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const criteria = {};
|
|
161
|
+
if (question.whenTrue !== undefined)
|
|
162
|
+
criteria.true = question.whenTrue;
|
|
163
|
+
if (question.whenFalse !== undefined)
|
|
164
|
+
criteria.false = question.whenFalse;
|
|
165
|
+
return {
|
|
166
|
+
type: "noul",
|
|
167
|
+
instructions: question.instructions,
|
|
168
|
+
...(Object.keys(criteria).length ? { criteria } : {}),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
export function toWireQuestions(questions) {
|
|
172
|
+
const wire = new Map();
|
|
173
|
+
for (const question of questions) {
|
|
174
|
+
if (wire.has(question.id)) {
|
|
175
|
+
throw new Error(`Duplicate question id: "${question.id}".`);
|
|
176
|
+
}
|
|
177
|
+
wire.set(question.id, toWireQuestion(question));
|
|
178
|
+
}
|
|
179
|
+
return Object.fromEntries(wire);
|
|
180
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import * as t from "typebox";
|
|
2
|
+
export const STRICT = { additionalProperties: false };
|
|
3
|
+
export const DEFAULT_BASE = "https://api.typesafe.ai";
|
|
4
|
+
export const DEFAULT_MODEL = "jev-latest";
|
|
5
|
+
/**
|
|
6
|
+
* Jev ingests the state once and then answers every question against it in
|
|
7
|
+
* parallel, so its context budget has two ceilings rather than one:
|
|
8
|
+
*
|
|
9
|
+
* - 64k tokens for state + all questions together
|
|
10
|
+
* - 32k tokens for state + the single longest question
|
|
11
|
+
*
|
|
12
|
+
* Tokens are not countable client-side, so both are checked against a
|
|
13
|
+
* chars/4 estimate. The check exists to turn a wasted round trip and an
|
|
14
|
+
* opaque 422 into an actionable local error.
|
|
15
|
+
*/
|
|
16
|
+
const TOTAL_TOKEN_LIMIT = 64_000;
|
|
17
|
+
const STATE_PLUS_QUESTION_TOKEN_LIMIT = 32_000;
|
|
18
|
+
const CHARS_PER_TOKEN = 4;
|
|
19
|
+
function config(ctx) {
|
|
20
|
+
return ctx.connection?.config ?? {};
|
|
21
|
+
}
|
|
22
|
+
function apiKey(ctx) {
|
|
23
|
+
const key = config(ctx).apiKey;
|
|
24
|
+
if (typeof key !== "string" || !key.trim()) {
|
|
25
|
+
throw new Error("Missing TYPESAFE_API_KEY. Create a key at https://console.typesafe.ai/settings/keys and store it as a secret.");
|
|
26
|
+
}
|
|
27
|
+
return key.trim();
|
|
28
|
+
}
|
|
29
|
+
function baseUrl(ctx) {
|
|
30
|
+
const raw = config(ctx).baseUrl;
|
|
31
|
+
const base = typeof raw === "string" && raw.trim() ? raw.trim() : DEFAULT_BASE;
|
|
32
|
+
return base.replace(/\/+$/, "");
|
|
33
|
+
}
|
|
34
|
+
export function resolveModel(ctx, override) {
|
|
35
|
+
if (typeof override === "string" && override.trim())
|
|
36
|
+
return override.trim();
|
|
37
|
+
const fallback = config(ctx).model;
|
|
38
|
+
if (typeof fallback === "string" && fallback.trim())
|
|
39
|
+
return fallback.trim();
|
|
40
|
+
return DEFAULT_MODEL;
|
|
41
|
+
}
|
|
42
|
+
function maxRetries(ctx) {
|
|
43
|
+
const value = Number(config(ctx).maxRetries);
|
|
44
|
+
if (!Number.isFinite(value) || value < 0)
|
|
45
|
+
return 2;
|
|
46
|
+
return Math.min(Math.floor(value), 5);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Every action in this plugin is a pure read, so a transient server-side
|
|
50
|
+
* failure can always be retried: no request can have half-happened. The
|
|
51
|
+
* documented pair is 429 and 529, but the service also answers 503
|
|
52
|
+
* `model_unavailable`, and nothing is gained by waiting for the next
|
|
53
|
+
* status to be discovered in production.
|
|
54
|
+
*/
|
|
55
|
+
function retryable(status) {
|
|
56
|
+
return status === 429 || status >= 500;
|
|
57
|
+
}
|
|
58
|
+
function retryDelayMs(response, attempt) {
|
|
59
|
+
const header = response.headers.get("retry-after");
|
|
60
|
+
const seconds = header ? Number(header) : Number.NaN;
|
|
61
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
62
|
+
return Math.min(seconds * 1000, 30_000);
|
|
63
|
+
}
|
|
64
|
+
return Math.min(250 * 2 ** attempt, 8_000);
|
|
65
|
+
}
|
|
66
|
+
function explain(status, body) {
|
|
67
|
+
const detail = body.slice(0, 500);
|
|
68
|
+
switch (status) {
|
|
69
|
+
case 401:
|
|
70
|
+
return `TypeSafe 401: the API key was rejected. ${detail}`;
|
|
71
|
+
case 422:
|
|
72
|
+
return `TypeSafe 422: the request body failed validation — a missing field or a malformed question. ${detail}`;
|
|
73
|
+
case 429:
|
|
74
|
+
return `TypeSafe 429: rate limit exceeded (250k tokens/sec, 1,200 req/min). Raise the connection's maxRetries or slow the caller. ${detail}`;
|
|
75
|
+
case 503:
|
|
76
|
+
return `TypeSafe 503: the model is unavailable. Transient — retry, and contact TypeSafe if it persists. ${detail}`;
|
|
77
|
+
case 529:
|
|
78
|
+
return `TypeSafe 529: the service is overloaded. Retry after a short delay. ${detail}`;
|
|
79
|
+
default:
|
|
80
|
+
return status >= 500
|
|
81
|
+
? `TypeSafe ${status}: the service failed this request. Transient — retry after a short delay. ${detail}`
|
|
82
|
+
: `TypeSafe ${status}: ${detail}`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function sleep(ms) {
|
|
86
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
87
|
+
}
|
|
88
|
+
export async function request(ctx, path, init = {}) {
|
|
89
|
+
const url = `${baseUrl(ctx)}${path}`;
|
|
90
|
+
const headers = new Headers(init.headers);
|
|
91
|
+
headers.set("authorization", `Bearer ${apiKey(ctx)}`);
|
|
92
|
+
headers.set("accept", "application/json");
|
|
93
|
+
if (init.body && !headers.has("content-type")) {
|
|
94
|
+
headers.set("content-type", "application/json");
|
|
95
|
+
}
|
|
96
|
+
const attempts = maxRetries(ctx);
|
|
97
|
+
for (let attempt = 0;; attempt++) {
|
|
98
|
+
const response = await fetch(url, { ...init, headers });
|
|
99
|
+
if (response.ok) {
|
|
100
|
+
const text = await response.text();
|
|
101
|
+
if (!text)
|
|
102
|
+
return {};
|
|
103
|
+
try {
|
|
104
|
+
return JSON.parse(text);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
throw new Error(`TypeSafe returned a non-JSON body: ${text.slice(0, 200)}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (retryable(response.status) && attempt < attempts) {
|
|
111
|
+
await sleep(retryDelayMs(response, attempt));
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
throw new Error(explain(response.status, await response.text()));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** Serialized length, measuring exactly what the request body will carry. */
|
|
118
|
+
function measure(value) {
|
|
119
|
+
return JSON.stringify(value ?? null).length;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Reject a request that cannot fit before it is paid for. Reported as an
|
|
123
|
+
* estimate, because it is one — the advice ("filter first") is the same
|
|
124
|
+
* advice that raises accuracy, so an early failure here is cheap.
|
|
125
|
+
*/
|
|
126
|
+
function assertWithinContext(state, questions) {
|
|
127
|
+
const stateTokens = measure(state) / CHARS_PER_TOKEN;
|
|
128
|
+
const questionTokens = Object.values(questions).map((question) => measure(question) / CHARS_PER_TOKEN);
|
|
129
|
+
const total = stateTokens + questionTokens.reduce((sum, n) => sum + n, 0);
|
|
130
|
+
const longest = stateTokens + Math.max(0, ...questionTokens);
|
|
131
|
+
if (total > TOTAL_TOKEN_LIMIT) {
|
|
132
|
+
throw new Error(`State plus questions is ~${Math.round(total)} tokens, over Jev's ${TOTAL_TOKEN_LIMIT}-token ceiling. ` +
|
|
133
|
+
"Retrieve and filter in code first, and send only the fields the questions need.");
|
|
134
|
+
}
|
|
135
|
+
if (longest > STATE_PLUS_QUESTION_TOKEN_LIMIT) {
|
|
136
|
+
throw new Error(`State plus the longest single question is ~${Math.round(longest)} tokens, over Jev's ` +
|
|
137
|
+
`${STATE_PLUS_QUESTION_TOKEN_LIMIT}-token per-question ceiling. Shorten the state or the question.`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
export async function evaluate(ctx, state, questions, model) {
|
|
141
|
+
if (!Object.keys(questions).length) {
|
|
142
|
+
throw new Error("At least one question is required.");
|
|
143
|
+
}
|
|
144
|
+
assertWithinContext(state, questions);
|
|
145
|
+
const body = await request(ctx, "/v1/systemone", {
|
|
146
|
+
method: "POST",
|
|
147
|
+
body: JSON.stringify({ state, model, questions }),
|
|
148
|
+
});
|
|
149
|
+
if (!body || typeof body.answers !== "object" || body.answers === null) {
|
|
150
|
+
throw new Error("TypeSafe returned a response with no answers map.");
|
|
151
|
+
}
|
|
152
|
+
return body;
|
|
153
|
+
}
|
|
154
|
+
export const stateSchema = t.Union([t.String(), t.Record(t.String(), t.Unknown()), t.Array(t.Unknown())], {
|
|
155
|
+
description: "The content every question is evaluated against. A plain string, or an object/array for records, chat logs, or application state. " +
|
|
156
|
+
"Send only what the decision needs: accuracy falls as unrelated detail grows (context rot), and a large state makes a wrong answer hard to attribute. " +
|
|
157
|
+
"Convert anything the model reads poorly before sending it — hex colors to names, timestamps to named buckets, computed numbers to their result. " +
|
|
158
|
+
"Ceilings: ~64k tokens for state plus all questions, ~32k for state plus the longest single question. " +
|
|
159
|
+
"State is data, not instructions: Jev does not treat it as hostile, so content written to steer a classifier can move the answer.",
|
|
160
|
+
});
|
|
161
|
+
export const modelSchema = t.String({
|
|
162
|
+
minLength: 1,
|
|
163
|
+
description: "Model that answers this call. Defaults to the connection's model, then 'jev-latest'. " +
|
|
164
|
+
"Aliases move when a release ships; pin a versioned id such as 'jev-1.13.0' if you have tuned thresholds against it. " +
|
|
165
|
+
"The response reports the versioned id that actually answered.",
|
|
166
|
+
});
|