@adaptiqai/deepseek-harness 0.2.2 → 0.2.4
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/lib/client.js +4 -4
- package/lib/index.js +291 -54
- package/lib/types/client/Connect.d.ts +25 -4
- package/lib/types/client/SettingsPage.d.ts +10 -1
- package/lib/types/client/pill.d.ts +59 -0
- package/lib/types/client/ui.d.ts +11 -5
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -140,7 +140,10 @@ function classifyFailure(status, raw, headers, model2) {
|
|
|
140
140
|
return make(CODES.invalidRequest, `AdaptiqAI couldn't accept this request${said}.`, null);
|
|
141
141
|
}
|
|
142
142
|
if (effective !== null && effective >= 500) {
|
|
143
|
-
|
|
143
|
+
const who = model2 ?? "AdaptiqAI";
|
|
144
|
+
if (effective === 503)
|
|
145
|
+
return make(CODES.server, `${model2 ?? "This model"} is offline right now. It will retry automatically.`, "retrying");
|
|
146
|
+
return make(CODES.server, `${who} didn't answer just now. It will retry automatically.`, "retrying");
|
|
144
147
|
}
|
|
145
148
|
if (effective === null) {
|
|
146
149
|
return make(CODES.server, detail.message !== null ? `AdaptiqAI stopped the reply: ${detail.message.slice(0, 200)}` : "AdaptiqAI stopped the reply.", null);
|
|
@@ -149,18 +152,29 @@ function classifyFailure(status, raw, headers, model2) {
|
|
|
149
152
|
}
|
|
150
153
|
function transportFailure(error) {
|
|
151
154
|
const name2 = error instanceof Error ? error.name : "";
|
|
152
|
-
const
|
|
155
|
+
const cause = error instanceof Error ? error.cause?.code : void 0;
|
|
156
|
+
const timeout = name2 === "TimeoutError" || cause === "UND_ERR_HEADERS_TIMEOUT" || cause === "UND_ERR_BODY_TIMEOUT";
|
|
153
157
|
return {
|
|
154
158
|
code: timeout ? CODES.timeout : CODES.transport,
|
|
155
|
-
message: timeout ? "AdaptiqAI took too long to answer. It will retry automatically." : "AdaptiqAI
|
|
159
|
+
message: timeout ? "AdaptiqAI took too long to answer. It will retry automatically." : "AdaptiqAI can't be reached right now. It will retry automatically.",
|
|
156
160
|
status: null,
|
|
157
161
|
requestId: null,
|
|
158
162
|
retryAfterMs: null,
|
|
159
163
|
serverType: null,
|
|
160
164
|
serverCode: null,
|
|
161
|
-
notice: "offline"
|
|
165
|
+
notice: timeout ? "retrying" : "offline"
|
|
162
166
|
};
|
|
163
167
|
}
|
|
168
|
+
function failureReach(failure) {
|
|
169
|
+
if (failure.code === CODES.transport)
|
|
170
|
+
return "unreachable";
|
|
171
|
+
if (failure.code === CODES.timeout)
|
|
172
|
+
return "transient";
|
|
173
|
+
return failure.status !== null && failure.status >= 500 && failure.status !== 507 ? "transient" : null;
|
|
174
|
+
}
|
|
175
|
+
function errorReach(error) {
|
|
176
|
+
return error instanceof AdaptiqHttpError ? failureReach(error.failure) : null;
|
|
177
|
+
}
|
|
164
178
|
var AdaptiqHttpError = class extends Error {
|
|
165
179
|
failure;
|
|
166
180
|
body;
|
|
@@ -699,6 +713,24 @@ function formatPercent(fraction) {
|
|
|
699
713
|
return "<1%";
|
|
700
714
|
return `${Math.min(100, Math.round(fraction * 100))}%`;
|
|
701
715
|
}
|
|
716
|
+
var QUIET_START_MS = 2e3;
|
|
717
|
+
var LANES_FRESH_MS = 3e4;
|
|
718
|
+
function waitKind(request, lanes, refreshedAt, now) {
|
|
719
|
+
if (request.phase === "preparing")
|
|
720
|
+
return "preparing";
|
|
721
|
+
if (request.phase === "queued")
|
|
722
|
+
return "lane";
|
|
723
|
+
if (request.phase !== "starting")
|
|
724
|
+
return null;
|
|
725
|
+
if (request.since !== null && now - request.since < QUIET_START_MS)
|
|
726
|
+
return null;
|
|
727
|
+
const total = lanes?.lanes ?? null;
|
|
728
|
+
const running = lanes?.runningNow ?? null;
|
|
729
|
+
const fresh = refreshedAt !== null && now - refreshedAt <= LANES_FRESH_MS;
|
|
730
|
+
if (request.laneHeld !== true && fresh && total !== null && total > 0 && running !== null && running >= total)
|
|
731
|
+
return "lane";
|
|
732
|
+
return "starting";
|
|
733
|
+
}
|
|
702
734
|
|
|
703
735
|
// ../core/lib/catalog.js
|
|
704
736
|
var obj = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
@@ -1243,6 +1275,11 @@ var str4 = (value) => typeof value === "string" && value.trim() !== "" ? value.t
|
|
|
1243
1275
|
var count2 = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
1244
1276
|
var DEFAULT_PREFERENCES = { rememberByDefault: true, welcomed: false };
|
|
1245
1277
|
var PAGE = 50;
|
|
1278
|
+
var TROUBLE_QUIET_MS = 6e4;
|
|
1279
|
+
var SESSION_BACKOFF_MAX_MS = 5 * 6e4;
|
|
1280
|
+
function sessionBackoffMs(failures) {
|
|
1281
|
+
return Math.min(SESSION_BACKOFF_MAX_MS, 3e4 * 2 ** Math.max(1, failures));
|
|
1282
|
+
}
|
|
1246
1283
|
function copyKey(source, target) {
|
|
1247
1284
|
return `${source}\0${target ?? ""}`;
|
|
1248
1285
|
}
|
|
@@ -1418,6 +1455,10 @@ var Workspace = class {
|
|
|
1418
1455
|
catalog = [];
|
|
1419
1456
|
catalogKey = "";
|
|
1420
1457
|
catalogAt = 0;
|
|
1458
|
+
/** The last `/v1/models` rows read: one stalled engine can fail the list for a while. */
|
|
1459
|
+
modelRows = [];
|
|
1460
|
+
/** Background reads have failed for a passing reason since `since`. */
|
|
1461
|
+
trouble = null;
|
|
1421
1462
|
offers = [];
|
|
1422
1463
|
checkoutMode = null;
|
|
1423
1464
|
/** Offer id → what the account summary says about it (model id, console link). */
|
|
@@ -1516,8 +1557,10 @@ var Workspace = class {
|
|
|
1516
1557
|
this.purchases.dispose();
|
|
1517
1558
|
clearTimeout(this.timer);
|
|
1518
1559
|
clearTimeout(this.attachTimer);
|
|
1519
|
-
for (const session of this.sessions.values())
|
|
1560
|
+
for (const session of this.sessions.values()) {
|
|
1520
1561
|
clearTimeout(session.timer);
|
|
1562
|
+
clearTimeout(session.waitTimer);
|
|
1563
|
+
}
|
|
1521
1564
|
this.listeners.clear();
|
|
1522
1565
|
}
|
|
1523
1566
|
/** Record that the provider route belongs to someone else. */
|
|
@@ -1635,7 +1678,8 @@ var Workspace = class {
|
|
|
1635
1678
|
return;
|
|
1636
1679
|
this.timer = setTimeout(() => {
|
|
1637
1680
|
void this.refresh().finally(() => {
|
|
1638
|
-
this.
|
|
1681
|
+
const normal = this.listeners.size > 0 ? this.deps.watchedRefreshMs : this.deps.refreshMs;
|
|
1682
|
+
this.schedule(this.trouble === null ? normal : Math.min(normal, 15e3 * 2 ** Math.min(this.trouble.failures - 1, 6)));
|
|
1639
1683
|
});
|
|
1640
1684
|
}, ms);
|
|
1641
1685
|
this.timer.unref?.();
|
|
@@ -1670,8 +1714,17 @@ var Workspace = class {
|
|
|
1670
1714
|
this.setCatalog([]);
|
|
1671
1715
|
return;
|
|
1672
1716
|
}
|
|
1673
|
-
|
|
1674
|
-
|
|
1717
|
+
let data;
|
|
1718
|
+
try {
|
|
1719
|
+
const rows = await this.deps.api.json("/v1/models");
|
|
1720
|
+
data = Array.isArray(rows?.data) ? rows.data : [];
|
|
1721
|
+
this.modelRows = data;
|
|
1722
|
+
} catch (error) {
|
|
1723
|
+
if (errorReach(error) === null || this.modelRows.length === 0)
|
|
1724
|
+
throw error;
|
|
1725
|
+
this.deps.log?.("warn", `adaptiq: model list unavailable, keeping the last one: ${error instanceof Error ? error.message : String(error)}`);
|
|
1726
|
+
data = this.modelRows;
|
|
1727
|
+
}
|
|
1675
1728
|
const summary2 = await this.loadSummary();
|
|
1676
1729
|
let availability;
|
|
1677
1730
|
if (summary2 !== null) {
|
|
@@ -1688,19 +1741,33 @@ var Workspace = class {
|
|
|
1688
1741
|
if (this.memory === null || !this.memory.available)
|
|
1689
1742
|
await this.loadFallbackMemory();
|
|
1690
1743
|
this.lastError = null;
|
|
1744
|
+
this.trouble = null;
|
|
1691
1745
|
this.refreshedAt = this.now();
|
|
1692
1746
|
this.evaluateNotices();
|
|
1693
1747
|
} catch (error) {
|
|
1694
1748
|
const failure = error instanceof AdaptiqHttpError ? error.failure : null;
|
|
1695
1749
|
this.lastError = { code: failure?.code ?? "UNKNOWN", message: failure?.message ?? (error instanceof Error ? error.message : String(error)), at: this.now() };
|
|
1696
|
-
|
|
1697
|
-
|
|
1750
|
+
const reach = failure === null ? null : failureReach(failure);
|
|
1751
|
+
if (reach !== null) {
|
|
1752
|
+
const since = this.trouble?.since ?? this.now();
|
|
1753
|
+
this.trouble = { since, reach, failures: (this.trouble?.failures ?? 0) + 1 };
|
|
1754
|
+
if (this.now() - since >= TROUBLE_QUIET_MS)
|
|
1755
|
+
this.raiseTrouble(reach);
|
|
1756
|
+
}
|
|
1698
1757
|
this.deps.log?.("warn", `adaptiq: refresh failed: ${this.lastError.message}`);
|
|
1699
1758
|
} finally {
|
|
1700
1759
|
this.loading = false;
|
|
1701
1760
|
this.publish();
|
|
1702
1761
|
}
|
|
1703
1762
|
}
|
|
1763
|
+
/**
|
|
1764
|
+
* Say, calmly, that background reads keep failing: "busy" while AdaptiqAI
|
|
1765
|
+
* answers with errors, "can't be reached" only when there is no connection.
|
|
1766
|
+
* @param reach - the kind of trouble.
|
|
1767
|
+
*/
|
|
1768
|
+
raiseTrouble(reach) {
|
|
1769
|
+
this.raise(reach === "unreachable" ? { kind: "offline", tone: "warning", title: "AdaptiqAI can't be reached", body: "Retrying automatically. Your models and conversations stay as they are.", actions: [{ id: "refresh", label: "Try again" }] } : { kind: "offline", tone: "info", title: "AdaptiqAI is busy \u2014 retrying", body: "Your models and conversations stay as they are. This usually clears by itself.", actions: [] });
|
|
1770
|
+
}
|
|
1704
1771
|
setCatalog(models) {
|
|
1705
1772
|
const key = JSON.stringify(models);
|
|
1706
1773
|
this.catalogAt = this.now();
|
|
@@ -1713,8 +1780,13 @@ var Workspace = class {
|
|
|
1713
1780
|
if (session.listeners.size === 0)
|
|
1714
1781
|
continue;
|
|
1715
1782
|
this.emitSession(session);
|
|
1716
|
-
|
|
1783
|
+
const awaited = session.backoff?.model ?? null;
|
|
1784
|
+
if (awaited !== null && !this.modelDown(awaited)) {
|
|
1785
|
+
session.backoff = null;
|
|
1786
|
+
this.pollSession(session, 0);
|
|
1787
|
+
} else if (!session.exists && session.remoteAt === 0) {
|
|
1717
1788
|
this.pollSession(session, 0);
|
|
1789
|
+
}
|
|
1718
1790
|
}
|
|
1719
1791
|
}
|
|
1720
1792
|
async loadSummary() {
|
|
@@ -2049,6 +2121,10 @@ var Workspace = class {
|
|
|
2049
2121
|
onAdaptiq: route === void 0 ? null : route.provider === this.deps.route,
|
|
2050
2122
|
phase: "idle",
|
|
2051
2123
|
phaseSince: null,
|
|
2124
|
+
inflight: 0,
|
|
2125
|
+
laneHeld: false,
|
|
2126
|
+
retry: null,
|
|
2127
|
+
failedNow: false,
|
|
2052
2128
|
model: route?.provider === this.deps.route ? route.model : null,
|
|
2053
2129
|
selected: null,
|
|
2054
2130
|
lastUsed: null,
|
|
@@ -2061,6 +2137,9 @@ var Workspace = class {
|
|
|
2061
2137
|
knowledge: [],
|
|
2062
2138
|
conversations: [],
|
|
2063
2139
|
remoteAt: 0,
|
|
2140
|
+
home: null,
|
|
2141
|
+
notOn: /* @__PURE__ */ new Set(),
|
|
2142
|
+
backoff: null,
|
|
2064
2143
|
listeners: /* @__PURE__ */ new Set()
|
|
2065
2144
|
};
|
|
2066
2145
|
this.sessions.set(id, session);
|
|
@@ -2090,6 +2169,7 @@ var Workspace = class {
|
|
|
2090
2169
|
remember: this.remember(id),
|
|
2091
2170
|
phase: session.phase,
|
|
2092
2171
|
phaseSince: session.phaseSince,
|
|
2172
|
+
waiting: this.waiting(session),
|
|
2093
2173
|
model: session.model,
|
|
2094
2174
|
modelName: this.modelName(session.model),
|
|
2095
2175
|
selectedModel: session.selected,
|
|
@@ -2137,13 +2217,29 @@ var Workspace = class {
|
|
|
2137
2217
|
clearTimeout(session.timer);
|
|
2138
2218
|
if (this.disposed || session.listeners.size === 0)
|
|
2139
2219
|
return;
|
|
2220
|
+
const wait = session.backoff === null ? 0 : session.backoff.until - this.now();
|
|
2140
2221
|
session.timer = setTimeout(() => {
|
|
2141
2222
|
void this.loadSession(session).finally(() => {
|
|
2142
2223
|
this.pollSession(session, sessionPollDelay(session));
|
|
2143
2224
|
});
|
|
2144
|
-
}, ms);
|
|
2225
|
+
}, Math.max(ms, wait));
|
|
2145
2226
|
session.timer.unref?.();
|
|
2146
2227
|
}
|
|
2228
|
+
/** @returns whether a model's instance is offline or disabled (not asked about conversations). */
|
|
2229
|
+
modelDown(id) {
|
|
2230
|
+
const status = this.catalog.find((entry) => entry.id === id)?.status;
|
|
2231
|
+
return status === "offline" || status === "disabled";
|
|
2232
|
+
}
|
|
2233
|
+
/**
|
|
2234
|
+
* Read a conversation less often after a failed read or while its model is
|
|
2235
|
+
* offline: 1, 2, 4, then every 5 minutes.
|
|
2236
|
+
* @param session - the conversation.
|
|
2237
|
+
* @param model - the offline model it waits for, or null after a failed read.
|
|
2238
|
+
*/
|
|
2239
|
+
backOff(session, model2) {
|
|
2240
|
+
const failures = (session.backoff?.failures ?? 0) + 1;
|
|
2241
|
+
session.backoff = { failures, until: this.now() + sessionBackoffMs(failures), model: model2 };
|
|
2242
|
+
}
|
|
2147
2243
|
/**
|
|
2148
2244
|
* The memory store a conversation's knowledge and memory live in: where it is
|
|
2149
2245
|
* stored once it exists, otherwise the model selected for it.
|
|
@@ -2230,42 +2326,23 @@ var Workspace = class {
|
|
|
2230
2326
|
return;
|
|
2231
2327
|
session.model = route.model;
|
|
2232
2328
|
}
|
|
2233
|
-
const
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
try {
|
|
2238
|
-
body = await this.deps.api.json(path, { allow: [404] });
|
|
2239
|
-
routed = true;
|
|
2240
|
-
} catch (error) {
|
|
2241
|
-
if (!(error instanceof AdaptiqHttpError) || error.status !== 409 && error.status !== 400)
|
|
2242
|
-
throw error;
|
|
2243
|
-
const models = obj4(obj4(error.body)?.detail)?.models;
|
|
2244
|
-
if (Array.isArray(models))
|
|
2245
|
-
suggested = models.filter((model2) => typeof model2 === "string" && model2 !== "");
|
|
2246
|
-
}
|
|
2247
|
-
const candidates = routed ? [] : session.model !== null ? [session.model] : suggested.length > 0 ? suggested.slice(0, 6) : [...this.catalog].sort((a, b) => Number(b.status === "running") - Number(a.status === "running")).map((entry) => entry.id).slice(0, 6);
|
|
2248
|
-
for (const model2 of candidates) {
|
|
2249
|
-
body = await this.deps.api.json(path, { headers: { "x-adaptiq-model": model2 }, allow: [404] });
|
|
2250
|
-
if (body !== null) {
|
|
2251
|
-
if (session.model === null) {
|
|
2252
|
-
session.model = model2;
|
|
2253
|
-
session.onAdaptiq ??= true;
|
|
2254
|
-
}
|
|
2255
|
-
break;
|
|
2256
|
-
}
|
|
2257
|
-
}
|
|
2258
|
-
if (routed && body !== null) {
|
|
2259
|
-
const served = str4(obj4(body.session)?.model) ?? str4(body.model);
|
|
2260
|
-
if (served !== null)
|
|
2261
|
-
session.model = served;
|
|
2262
|
-
else if (session.model === null && session.lastUsed !== null)
|
|
2263
|
-
session.model = session.lastUsed;
|
|
2264
|
-
session.onAdaptiq ??= true;
|
|
2329
|
+
const found = await this.findSession(session);
|
|
2330
|
+
if (found === void 0) {
|
|
2331
|
+
session.remoteAt = this.now();
|
|
2332
|
+
return;
|
|
2265
2333
|
}
|
|
2266
|
-
|
|
2334
|
+
session.backoff = null;
|
|
2335
|
+
if (found.asked)
|
|
2267
2336
|
session.remoteAt = this.now();
|
|
2337
|
+
const body = found.body;
|
|
2268
2338
|
if (body === null) {
|
|
2339
|
+
if (session.exists) {
|
|
2340
|
+
session.memoryTokens = null;
|
|
2341
|
+
session.saved = null;
|
|
2342
|
+
session.optimizing = false;
|
|
2343
|
+
session.revision = null;
|
|
2344
|
+
session.conversations = [];
|
|
2345
|
+
}
|
|
2269
2346
|
session.exists = false;
|
|
2270
2347
|
session.knowledge = [];
|
|
2271
2348
|
return;
|
|
@@ -2289,6 +2366,7 @@ var Workspace = class {
|
|
|
2289
2366
|
session.conversations = await this.attachedConversations(session).catch(() => session.conversations);
|
|
2290
2367
|
} catch (error) {
|
|
2291
2368
|
session.remoteAt = this.now();
|
|
2369
|
+
this.backOff(session, null);
|
|
2292
2370
|
this.deps.log?.("warn", `adaptiq: could not read conversation ${session.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2293
2371
|
if (!(error instanceof AdaptiqHttpError))
|
|
2294
2372
|
throw error;
|
|
@@ -2304,6 +2382,83 @@ var Workspace = class {
|
|
|
2304
2382
|
session.loading = void 0;
|
|
2305
2383
|
}
|
|
2306
2384
|
}
|
|
2385
|
+
/**
|
|
2386
|
+
* Find a conversation on the server. Once a model has answered for it, only
|
|
2387
|
+
* that model is asked. Before that, the session-aware route (which needs no
|
|
2388
|
+
* model), then, on gateways that want one, the conversation's model or the
|
|
2389
|
+
* models the gateway names: never a model that is offline or that already
|
|
2390
|
+
* said the conversation is not there.
|
|
2391
|
+
* @param session - the conversation.
|
|
2392
|
+
* @returns its body (null when the server does not have it) and whether
|
|
2393
|
+
* anything could be asked, or undefined when its model is offline (backed off).
|
|
2394
|
+
*/
|
|
2395
|
+
async findSession(session) {
|
|
2396
|
+
const path = `/v1/sessions/${encodeURIComponent(session.id)}`;
|
|
2397
|
+
const ask = async (model2) => {
|
|
2398
|
+
try {
|
|
2399
|
+
return await this.deps.api.json(path, { headers: { "x-adaptiq-model": model2 }, allow: [404] });
|
|
2400
|
+
} catch (error) {
|
|
2401
|
+
if (error instanceof AdaptiqHttpError && error.status === 409)
|
|
2402
|
+
return null;
|
|
2403
|
+
throw error;
|
|
2404
|
+
}
|
|
2405
|
+
};
|
|
2406
|
+
if (session.home !== null) {
|
|
2407
|
+
const home = session.home;
|
|
2408
|
+
if (this.modelDown(home)) {
|
|
2409
|
+
this.backOff(session, home);
|
|
2410
|
+
return void 0;
|
|
2411
|
+
}
|
|
2412
|
+
const body = await ask(home);
|
|
2413
|
+
if (body === null) {
|
|
2414
|
+
session.home = null;
|
|
2415
|
+
session.notOn.add(home);
|
|
2416
|
+
} else
|
|
2417
|
+
session.model = home;
|
|
2418
|
+
return { body, asked: true };
|
|
2419
|
+
}
|
|
2420
|
+
let suggested = [];
|
|
2421
|
+
try {
|
|
2422
|
+
const body = await this.deps.api.json(path, { allow: [404] });
|
|
2423
|
+
if (body !== null) {
|
|
2424
|
+
const served = str4(obj4(body.session)?.model) ?? str4(body.model);
|
|
2425
|
+
if (served !== null) {
|
|
2426
|
+
session.model = served;
|
|
2427
|
+
session.home = served;
|
|
2428
|
+
} else if (session.model === null && session.lastUsed !== null)
|
|
2429
|
+
session.model = session.lastUsed;
|
|
2430
|
+
session.onAdaptiq ??= true;
|
|
2431
|
+
}
|
|
2432
|
+
return { body, asked: true };
|
|
2433
|
+
} catch (error) {
|
|
2434
|
+
if (!(error instanceof AdaptiqHttpError) || error.status !== 409 && error.status !== 400)
|
|
2435
|
+
throw error;
|
|
2436
|
+
const models = obj4(obj4(error.body)?.detail)?.models;
|
|
2437
|
+
if (Array.isArray(models))
|
|
2438
|
+
suggested = models.filter((model2) => typeof model2 === "string" && model2 !== "");
|
|
2439
|
+
}
|
|
2440
|
+
const listed = session.model !== null ? [session.model] : suggested.length > 0 ? suggested : [...this.catalog].sort((a, b) => Number(b.status === "running") - Number(a.status === "running")).map((entry) => entry.id);
|
|
2441
|
+
const open = listed.filter((model2) => !session.notOn.has(model2)).slice(0, 6);
|
|
2442
|
+
const candidates = open.filter((model2) => !this.modelDown(model2));
|
|
2443
|
+
for (const model2 of candidates) {
|
|
2444
|
+
const body = await ask(model2);
|
|
2445
|
+
if (body !== null) {
|
|
2446
|
+
session.home = model2;
|
|
2447
|
+
if (session.model === null) {
|
|
2448
|
+
session.model = model2;
|
|
2449
|
+
session.onAdaptiq ??= true;
|
|
2450
|
+
}
|
|
2451
|
+
return { body, asked: true };
|
|
2452
|
+
}
|
|
2453
|
+
session.notOn.add(model2);
|
|
2454
|
+
}
|
|
2455
|
+
const offline = open.find((model2) => this.modelDown(model2));
|
|
2456
|
+
if (offline !== void 0) {
|
|
2457
|
+
this.backOff(session, offline);
|
|
2458
|
+
return void 0;
|
|
2459
|
+
}
|
|
2460
|
+
return { body: null, asked: candidates.length > 0 };
|
|
2461
|
+
}
|
|
2307
2462
|
/**
|
|
2308
2463
|
* Choose whether new messages of a conversation are saved.
|
|
2309
2464
|
* @param id - harness session id.
|
|
@@ -2322,6 +2477,38 @@ var Workspace = class {
|
|
|
2322
2477
|
session.phase = phase;
|
|
2323
2478
|
session.phaseSince = this.now();
|
|
2324
2479
|
this.emitSession(session);
|
|
2480
|
+
this.watchWait(session);
|
|
2481
|
+
}
|
|
2482
|
+
/** What the conversation's request waits for, as the composer says it. */
|
|
2483
|
+
waiting(session) {
|
|
2484
|
+
const entry = this.catalog.find((item) => item.id === session.model);
|
|
2485
|
+
return waitKind({ phase: session.phase, since: session.phaseSince, laneHeld: session.laneHeld }, entry ?? null, this.refreshedAt, this.now());
|
|
2486
|
+
}
|
|
2487
|
+
/**
|
|
2488
|
+
* A request that stays quiet past {@link QUIET_START_MS} gets a word: look
|
|
2489
|
+
* again then, with the lane counts read fresh when they are older than that.
|
|
2490
|
+
*/
|
|
2491
|
+
watchWait(session) {
|
|
2492
|
+
clearTimeout(session.waitTimer);
|
|
2493
|
+
if (session.phase !== "starting" || this.disposed)
|
|
2494
|
+
return;
|
|
2495
|
+
const check = () => {
|
|
2496
|
+
if (session.phase !== "starting" || this.disposed)
|
|
2497
|
+
return;
|
|
2498
|
+
this.emitSession(session);
|
|
2499
|
+
const stale = this.refreshedAt === null || this.now() - this.refreshedAt > 1e4;
|
|
2500
|
+
if (stale && !session.laneHeld && this.deps.auth.phase() === "connected") {
|
|
2501
|
+
void this.refresh().then(() => {
|
|
2502
|
+
if (session.phase === "starting")
|
|
2503
|
+
this.emitSession(session);
|
|
2504
|
+
}, () => {
|
|
2505
|
+
});
|
|
2506
|
+
}
|
|
2507
|
+
session.waitTimer = setTimeout(check, 15e3);
|
|
2508
|
+
session.waitTimer.unref?.();
|
|
2509
|
+
};
|
|
2510
|
+
session.waitTimer = setTimeout(check, QUIET_START_MS);
|
|
2511
|
+
session.waitTimer.unref?.();
|
|
2325
2512
|
}
|
|
2326
2513
|
// RequestEvents — the adapter reports each request here.
|
|
2327
2514
|
started(sessionId2, model2) {
|
|
@@ -2330,7 +2517,13 @@ var Workspace = class {
|
|
|
2330
2517
|
const session = this.session(sessionId2);
|
|
2331
2518
|
session.onAdaptiq = true;
|
|
2332
2519
|
session.model = model2;
|
|
2333
|
-
|
|
2520
|
+
session.notOn.delete(model2);
|
|
2521
|
+
session.backoff = null;
|
|
2522
|
+
session.inflight++;
|
|
2523
|
+
session.laneHeld = false;
|
|
2524
|
+
session.failedNow = false;
|
|
2525
|
+
const retry = session.retry !== null && session.retry.until > this.now() ? session.retry : null;
|
|
2526
|
+
this.setPhase(session, retry?.phase ?? "starting");
|
|
2334
2527
|
}
|
|
2335
2528
|
progress(sessionId2, stage) {
|
|
2336
2529
|
if (sessionId2 === void 0)
|
|
@@ -2340,14 +2533,26 @@ var Workspace = class {
|
|
|
2340
2533
|
return;
|
|
2341
2534
|
if (stage === "inference.queued")
|
|
2342
2535
|
this.setPhase(session, "queued");
|
|
2343
|
-
else if (stage === "context.preparing"
|
|
2536
|
+
else if (stage === "context.preparing")
|
|
2344
2537
|
this.setPhase(session, "preparing");
|
|
2538
|
+
else if (stage === "context.ready" || stage === "inference.ready") {
|
|
2539
|
+
if (stage === "inference.ready")
|
|
2540
|
+
session.laneHeld = true;
|
|
2541
|
+
session.retry = null;
|
|
2542
|
+
if (session.phase === "starting")
|
|
2543
|
+
this.emitSession(session);
|
|
2544
|
+
else
|
|
2545
|
+
this.setPhase(session, "starting");
|
|
2546
|
+
}
|
|
2345
2547
|
}
|
|
2346
2548
|
output(sessionId2) {
|
|
2347
2549
|
if (sessionId2 === void 0)
|
|
2348
2550
|
return;
|
|
2349
|
-
this.
|
|
2350
|
-
|
|
2551
|
+
const session = this.session(sessionId2);
|
|
2552
|
+
session.laneHeld = true;
|
|
2553
|
+
session.retry = null;
|
|
2554
|
+
this.setPhase(session, "responding");
|
|
2555
|
+
this.clearNotices("busy", "preparing", "offline", "retrying");
|
|
2351
2556
|
}
|
|
2352
2557
|
usage(sessionId2, model2, usage2) {
|
|
2353
2558
|
if (sessionId2 === void 0)
|
|
@@ -2382,8 +2587,13 @@ var Workspace = class {
|
|
|
2382
2587
|
message: failure.message
|
|
2383
2588
|
});
|
|
2384
2589
|
this.failures.length = Math.min(this.failures.length, 12);
|
|
2385
|
-
if (sessionId2 !== void 0)
|
|
2386
|
-
this.
|
|
2590
|
+
if (sessionId2 !== void 0) {
|
|
2591
|
+
const session = this.session(sessionId2);
|
|
2592
|
+
session.failedNow = true;
|
|
2593
|
+
const waits = RETRYABLE_CODES.includes(failure.code) && (failure.notice === "busy" || failure.notice === "preparing");
|
|
2594
|
+
session.retry = waits ? { phase: failure.notice === "busy" ? "queued" : "preparing", until: this.now() + (failure.retryAfterMs ?? 3e4) + 15e3 } : null;
|
|
2595
|
+
this.setPhase(session, session.retry?.phase ?? "interrupted");
|
|
2596
|
+
}
|
|
2387
2597
|
const entry = this.catalog.find((item) => item.id === model2);
|
|
2388
2598
|
const name2 = entry?.name ?? model2;
|
|
2389
2599
|
switch (failure.notice) {
|
|
@@ -2425,8 +2635,20 @@ var Workspace = class {
|
|
|
2425
2635
|
case "model-unavailable":
|
|
2426
2636
|
this.raise({ kind: "model-unavailable", tone: "warning", title: `${name2} isn't available`, body: failure.message, actions: [{ id: "open-models", label: "See your models", url: this.links.models }] });
|
|
2427
2637
|
break;
|
|
2638
|
+
case "retrying": {
|
|
2639
|
+
const offline = entry?.status === "offline";
|
|
2640
|
+
this.raise({
|
|
2641
|
+
kind: "retrying",
|
|
2642
|
+
tone: "info",
|
|
2643
|
+
title: offline ? `${name2} is offline` : `${name2} didn't answer just now`,
|
|
2644
|
+
body: offline ? "Your request retries by itself. Your other AdaptiqAI models still work; pick one to continue now." : "Your request retries by itself.",
|
|
2645
|
+
actions: [],
|
|
2646
|
+
...sessionId2 === void 0 ? {} : { sessionId: sessionId2 }
|
|
2647
|
+
});
|
|
2648
|
+
break;
|
|
2649
|
+
}
|
|
2428
2650
|
case "offline":
|
|
2429
|
-
this.raise({ kind: "offline", tone: "warning", title: "AdaptiqAI can't be reached", body:
|
|
2651
|
+
this.raise({ kind: "offline", tone: "warning", title: "AdaptiqAI can't be reached", body: "Your request retries by itself once the connection is back.", actions: [], ...sessionId2 === void 0 ? {} : { sessionId: sessionId2 } });
|
|
2430
2652
|
break;
|
|
2431
2653
|
default:
|
|
2432
2654
|
break;
|
|
@@ -2436,8 +2658,23 @@ var Workspace = class {
|
|
|
2436
2658
|
if (sessionId2 === void 0)
|
|
2437
2659
|
return;
|
|
2438
2660
|
const session = this.session(sessionId2);
|
|
2439
|
-
|
|
2440
|
-
|
|
2661
|
+
session.inflight = Math.max(0, session.inflight - 1);
|
|
2662
|
+
if (session.failedNow && session.retry !== null) {
|
|
2663
|
+
const retry = session.retry;
|
|
2664
|
+
clearTimeout(session.waitTimer);
|
|
2665
|
+
session.waitTimer = setTimeout(() => {
|
|
2666
|
+
if (session.inflight === 0 && session.retry === retry) {
|
|
2667
|
+
session.retry = null;
|
|
2668
|
+
this.setPhase(session, "interrupted");
|
|
2669
|
+
}
|
|
2670
|
+
}, Math.max(0, retry.until - this.now()));
|
|
2671
|
+
session.waitTimer.unref?.();
|
|
2672
|
+
} else {
|
|
2673
|
+
if (!session.failedNow)
|
|
2674
|
+
session.retry = null;
|
|
2675
|
+
if (session.phase !== "interrupted")
|
|
2676
|
+
this.setPhase(session, "idle");
|
|
2677
|
+
}
|
|
2441
2678
|
if (session.listeners.size > 0) {
|
|
2442
2679
|
setTimeout(() => {
|
|
2443
2680
|
void this.loadSession(session);
|
|
@@ -1,18 +1,39 @@
|
|
|
1
|
-
import type { AccountSnapshot } from '@adaptiqai/core/types';
|
|
1
|
+
import type { AccountPhase, AccountSnapshot } from '@adaptiqai/core/types';
|
|
2
2
|
import type { Kit } from './actions.ts';
|
|
3
3
|
import { type T } from './ui.tsx';
|
|
4
|
+
/** How long "Connected" stays before the card moves on by itself. */
|
|
5
|
+
export declare const CONNECTED_MOMENT_MS = 1500;
|
|
6
|
+
/**
|
|
7
|
+
* The moment right after the browser approval: true from the switch
|
|
8
|
+
* signing-in → connected until {@link CONNECTED_MOMENT_MS} later or dismiss.
|
|
9
|
+
* @param phase - the account phase, or null before it is known.
|
|
10
|
+
* @returns whether to show it, and how to end it early.
|
|
11
|
+
*/
|
|
12
|
+
export declare function useConnectedMoment(phase: AccountPhase | null, ms?: number): {
|
|
13
|
+
show: boolean;
|
|
14
|
+
dismiss: () => void;
|
|
15
|
+
};
|
|
16
|
+
/** "Connected to <workspace>": the stones settle, a check lands. Clicking anywhere ends it. */
|
|
17
|
+
export declare function ConnectedMoment({ t, workspace, onClose }: {
|
|
18
|
+
t: T;
|
|
19
|
+
workspace: string | null;
|
|
20
|
+
onClose: () => void;
|
|
21
|
+
}): import("react").JSX.Element;
|
|
4
22
|
/** The code split in two halves of boxed characters. */
|
|
5
23
|
export declare function DeviceCode({ code }: {
|
|
6
24
|
code: string;
|
|
7
25
|
}): import("react").JSX.Element;
|
|
8
26
|
/**
|
|
9
27
|
* Hero content for each sign-in state. Lives on a navy hero surface.
|
|
10
|
-
* @param props - account snapshot, kit, translate.
|
|
28
|
+
* @param props - account snapshot, kit, translate, and a quiet second action (Later, Cancel).
|
|
11
29
|
* @returns the hero body.
|
|
12
30
|
*/
|
|
13
|
-
export declare function ConnectHero({ account, kit, t,
|
|
31
|
+
export declare function ConnectHero({ account, kit, t, aside }: {
|
|
14
32
|
account: AccountSnapshot | null;
|
|
15
33
|
kit: Kit;
|
|
16
34
|
t: T;
|
|
17
|
-
|
|
35
|
+
aside?: {
|
|
36
|
+
label: string;
|
|
37
|
+
onClick: () => void;
|
|
38
|
+
};
|
|
18
39
|
}): import("react").JSX.Element;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AccountSnapshot, ModelView } from '@adaptiqai/core/types';
|
|
1
|
+
import type { AccountSnapshot, MemoryView, ModelView } from '@adaptiqai/core/types';
|
|
2
2
|
import type { Kit } from './actions.ts';
|
|
3
3
|
import { type T } from './ui.tsx';
|
|
4
4
|
type Hook<S> = <R>(select: (snapshot: S) => R) => R;
|
|
@@ -7,6 +7,15 @@ export interface SettingsProps {
|
|
|
7
7
|
kit: Kit;
|
|
8
8
|
useAqAccount: Hook<AccountSnapshot | null>;
|
|
9
9
|
}
|
|
10
|
+
/** The fullness chip: green below 70 %, amber to 90 %, red above. */
|
|
11
|
+
export declare function MemoryChip({ store }: {
|
|
12
|
+
store: MemoryView;
|
|
13
|
+
}): import("react").JSX.Element;
|
|
14
|
+
/** The store's bar, in the same colour as its chip. */
|
|
15
|
+
export declare function MemoryBar({ store, thin }: {
|
|
16
|
+
store: MemoryView;
|
|
17
|
+
thin?: boolean;
|
|
18
|
+
}): import("react").JSX.Element;
|
|
10
19
|
/**
|
|
11
20
|
* A model's facts, the same fields in the same order for every model:
|
|
12
21
|
* memory, thinking levels, region. A field is left out only when unknown.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** The composer pill's words and timing, kept apart from React so they can be tested. */
|
|
2
|
+
import type { WaitKind } from '@adaptiqai/core/types';
|
|
3
|
+
type T = (key: string, params?: Record<string, unknown>) => string;
|
|
4
|
+
/** How long "+24.6k from memory" stays after a reply. */
|
|
5
|
+
export declare const AFTER_REPLY_MS = 4000;
|
|
6
|
+
/** How long the count-up to that figure takes. */
|
|
7
|
+
export declare const COUNT_UP_MS = 900;
|
|
8
|
+
/**
|
|
9
|
+
* The pill's words while a request waits.
|
|
10
|
+
* @param t - translate.
|
|
11
|
+
* @param kind - what the request waits for.
|
|
12
|
+
* @returns e.g. "Waiting for a free lane".
|
|
13
|
+
*/
|
|
14
|
+
export declare function waitText(t: T, kind: WaitKind): string;
|
|
15
|
+
/**
|
|
16
|
+
* The pill's words after a reply, split around the figure so it can be set heavier.
|
|
17
|
+
* @param t - translate.
|
|
18
|
+
* @param fromMemory - tokens the reply read from memory (shown so far, while counting up).
|
|
19
|
+
* @returns e.g. { before: '', figure: '+24.6k', after: ' from memory' }.
|
|
20
|
+
*/
|
|
21
|
+
export declare function afterReplyParts(t: T, fromMemory: number): {
|
|
22
|
+
before: string;
|
|
23
|
+
figure: string;
|
|
24
|
+
after: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The pill's words after a reply.
|
|
28
|
+
* @param t - translate.
|
|
29
|
+
* @param fromMemory - tokens the reply read from memory.
|
|
30
|
+
* @returns e.g. "+24.6k from memory".
|
|
31
|
+
*/
|
|
32
|
+
export declare function afterReplyText(t: T, fromMemory: number): string;
|
|
33
|
+
/**
|
|
34
|
+
* The figure partway through the count-up: fast at first, settling softly.
|
|
35
|
+
* @param target - the final figure.
|
|
36
|
+
* @param elapsed - ms since the count-up began.
|
|
37
|
+
* @param duration - ms the count-up takes.
|
|
38
|
+
* @returns the figure to show now.
|
|
39
|
+
*/
|
|
40
|
+
export declare function countUp(target: number, elapsed: number, duration?: number): number;
|
|
41
|
+
/**
|
|
42
|
+
* Follows a conversation's last turn and reports a new reply that read from
|
|
43
|
+
* memory for {@link AFTER_REPLY_MS}. The turn already there when the view
|
|
44
|
+
* first arrives is history, not news.
|
|
45
|
+
*/
|
|
46
|
+
export declare class AfterReply {
|
|
47
|
+
private readonly onChange;
|
|
48
|
+
private readonly ms;
|
|
49
|
+
private baseline;
|
|
50
|
+
private timer;
|
|
51
|
+
constructor(onChange: (fromMemory: number | null) => void, ms?: number);
|
|
52
|
+
/** The conversation's view arrived or changed. */
|
|
53
|
+
seen(turn: {
|
|
54
|
+
readonly at: number;
|
|
55
|
+
readonly fromMemory: number;
|
|
56
|
+
} | null): void;
|
|
57
|
+
dispose(): void;
|
|
58
|
+
}
|
|
59
|
+
export {};
|