aegis-desktop 0.8.17 → 0.8.19
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/local/engine.js +114 -39
- package/lib/local/shell.js +393 -39
- package/lib/local/tools.js +156 -13
- package/lib/settings.js +36 -1
- package/package.json +1 -1
- package/renderer/app.js +227 -0
- package/renderer/budget.js +13 -0
- package/renderer/index.html +82 -0
- package/renderer/style.css +74 -0
- package/renderer/usage.js +30 -0
- package/vendor/aegis.js +13 -0
- package/vendor/agent-gate.mjs +81 -13
- package/vendor/authorship-key.js +45 -4
- package/vendor/byok-catalog.js +49 -1
- package/vendor/credentials.js +94 -6
- package/vendor/env-file.js +161 -5
- package/vendor/hooks.js +427 -0
- package/vendor/platform.js +410 -0
package/lib/local/engine.js
CHANGED
|
@@ -240,6 +240,57 @@ function providerKeyFromEnv(provider) {
|
|
|
240
240
|
const DEEPSEEK_REASONING_MODEL_RE = /^deepseek-(v4(\.\d+)?-(flash|pro)|flash|pro|reasoner)$/;
|
|
241
241
|
const EFFORT_TOKEN_BUDGET = { low: 8192, medium: 16384, high: 32768 };
|
|
242
242
|
|
|
243
|
+
/**
|
|
244
|
+
* DeepSeek native reasoning level per config rung.
|
|
245
|
+
*
|
|
246
|
+
* The field is `reasoning_effort`, NOT `effort`. Verified live 2026-10-02: a
|
|
247
|
+
* DeepSeek body carrying `effort` returns HTTP 200 and is ignored, while
|
|
248
|
+
* `reasoning_effort` binds and rejects an unknown value with a 422 naming its
|
|
249
|
+
* enum (none|minimal|low|medium|high|xhigh|ultra|max). Behaviourally,
|
|
250
|
+
* `reasoning_effort:"none"` produced 0 reasoning_content chars and `"max"`
|
|
251
|
+
* produced 138, whereas `effort:"none"` still produced 112 — thinking stayed
|
|
252
|
+
* on. So the old field was inert on every turn, and inertness is the worst
|
|
253
|
+
* failure here: it 200s, nothing surfaces, and the model merely thinks at the
|
|
254
|
+
* wrong depth.
|
|
255
|
+
*
|
|
256
|
+
* Every rung of the knob has a namesake in that enum, so the map is 1:1 rather
|
|
257
|
+
* than the earlier (low|max, no medium) fold, which rested on the mistaken
|
|
258
|
+
* belief that DeepSeek had no `medium`. `max_tokens` (EFFORT_TOKEN_BUDGET)
|
|
259
|
+
* still scales as the output ceiling, since hidden chain-of-thought shares that
|
|
260
|
+
* budget on DeepSeek.
|
|
261
|
+
*
|
|
262
|
+
* Mirrors the upstream engine's src/backend.js DEEPSEEK_EFFORT verbatim;
|
|
263
|
+
* desktop/renderer/budget.js carries the renderer's copy for the same
|
|
264
|
+
* no-drift guarantee as DEEPSEEK_REASONING_MODEL_RE above.
|
|
265
|
+
*/
|
|
266
|
+
const DEEPSEEK_EFFORT = { low: 'low', medium: 'medium', high: 'high' };
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Per-family effort translation, mirroring the upstream engine's
|
|
270
|
+
* EFFORT_DIALECTS. The knob is one rung here (low|medium|high) but it is NOT
|
|
271
|
+
* portable across vendors, and passing it through untranslated is a correctness
|
|
272
|
+
* bug rather than a no-op: Groq 400s any level outside a model's supported set,
|
|
273
|
+
* and Gemini 3 fails on `medium` outright while low/high succeed. So each
|
|
274
|
+
* family states its own wire field and level map, and a family with no such
|
|
275
|
+
* field gets NOTHING — an unknown body field either 400s or is silently
|
|
276
|
+
* dropped, and both are worse than stating no opinion.
|
|
277
|
+
*
|
|
278
|
+
* xAI's `*-non-reasoning` row is left alone entirely: the model exists to not
|
|
279
|
+
* reason, and sending it an effort field is precisely the request the user
|
|
280
|
+
* picked that row to avoid. grok-4.20 and gemini-2.0 stay bare too — no
|
|
281
|
+
* documented level field exists for them, and guessing on a strict validator
|
|
282
|
+
* is a 400 rather than an upgrade.
|
|
283
|
+
*/
|
|
284
|
+
const EFFORT_IDENTITY = { low: 'low', medium: 'medium', high: 'high' };
|
|
285
|
+
const EFFORT_INERT_RE = /non-reasoning/i;
|
|
286
|
+
const EFFORT_DIALECTS = [
|
|
287
|
+
{ provider: 'deepseek', re: DEEPSEEK_REASONING_MODEL_RE, field: 'reasoning_effort', map: DEEPSEEK_EFFORT },
|
|
288
|
+
{ provider: 'openai', re: /^(gpt-5|o[0-9])/i, field: 'reasoning_effort', map: EFFORT_IDENTITY },
|
|
289
|
+
{ provider: 'groq', re: /^(openai\/gpt-oss|qwen\/qwen3)/i, field: 'reasoning_effort', map: EFFORT_IDENTITY },
|
|
290
|
+
{ provider: 'xai', re: /^grok-(4\.[5-9]|[5-9])/, field: 'reasoning_effort', map: EFFORT_IDENTITY },
|
|
291
|
+
{ provider: 'google', re: /^gemini-(2\.5|3|[4-9])/, field: 'reasoning_effort', map: { ...EFFORT_IDENTITY, medium: 'high' } },
|
|
292
|
+
];
|
|
293
|
+
|
|
243
294
|
/**
|
|
244
295
|
* Idle-stream budget for a pooled brain call ("work autonomously"). The
|
|
245
296
|
* generic watchdog in vendor/aegis.js kills a stream that goes 60s without a
|
|
@@ -296,6 +347,24 @@ function reasoningBudget(model, maxTokens, effort) {
|
|
|
296
347
|
return undefined;
|
|
297
348
|
}
|
|
298
349
|
|
|
350
|
+
/**
|
|
351
|
+
* The native effort verdict for this provider+model: `{ field, level }`, or
|
|
352
|
+
* `undefined` when the family has no effort knob. `undefined` means "say
|
|
353
|
+
* nothing", never "send a default" — inventing a level is how the knob silently
|
|
354
|
+
* overrides the vendor's own default. Supersedes the DeepSeek-only
|
|
355
|
+
* deepseekEffort() it replaces: same DeepSeek mapping, now stated for every
|
|
356
|
+
* family that has one instead of only the one that used to be wired.
|
|
357
|
+
*/
|
|
358
|
+
function nativeEffort(provider, model, effort) {
|
|
359
|
+
const m = String(model || '');
|
|
360
|
+
if (EFFORT_INERT_RE.test(m)) return undefined;
|
|
361
|
+
const eff = effort === 'low' || effort === 'medium' ? effort : 'high';
|
|
362
|
+
for (const d of EFFORT_DIALECTS) {
|
|
363
|
+
if (d.provider === provider && d.re.test(m)) return { field: d.field, level: d.map[eff] };
|
|
364
|
+
}
|
|
365
|
+
return undefined;
|
|
366
|
+
}
|
|
367
|
+
|
|
299
368
|
/** Relay model entries arrive as ids or objects; keep only real model ids. */
|
|
300
369
|
function normalizeCatalog(models) {
|
|
301
370
|
if (!Array.isArray(models)) return [];
|
|
@@ -307,35 +376,32 @@ function normalizeCatalog(models) {
|
|
|
307
376
|
/**
|
|
308
377
|
* The Aegis Cloud (pooled) dropdown.
|
|
309
378
|
*
|
|
310
|
-
* It offers
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
379
|
+
* It offers the server's whole catalog (`/api/v1/models`): the pooled brain
|
|
380
|
+
* first, under its one name (Nexus), then every per-provider id the account can
|
|
381
|
+
* pin (`deepseek`, `anthropic`, `groq`, `openai`, ...).
|
|
382
|
+
*
|
|
383
|
+
* Two narrower shapes were shipped here and both are reverted, because both
|
|
384
|
+
* produced the same report — a user asking where the models went. First the
|
|
385
|
+
* class collapsed to a single "Nexus" row (`filterAegisCatalog`), which ended
|
|
386
|
+
* the two-host disagreement about the brain's five tier spellings by removing
|
|
387
|
+
* the choice: a funded account had `deepseek` advertised to it and no way to
|
|
388
|
+
* pin it. Then it offered nothing at all — `omitAegisCloudModels` composed with
|
|
389
|
+
* that collapse returns `[]`, because the omission removes the only entry the
|
|
390
|
+
* collapse looks for. The rule both hosts call now is `offerableCatalog`:
|
|
391
|
+
* everything the server advertises, the tier spellings folded into the one row
|
|
392
|
+
* they point at.
|
|
319
393
|
*
|
|
320
394
|
* What is NOT changed: the account key, and everything the key is *for* — the
|
|
321
395
|
* pooled turn itself, cloud memory sync, and the BYOK handling fee that is
|
|
322
|
-
* billed to this account (see the byok branch below
|
|
323
|
-
* provider id the server's catalog names). Omitting a model list is not
|
|
324
|
-
* disconnecting the lane.
|
|
325
|
-
*
|
|
326
|
-
* The omission itself is one shared rule (`omitAegisCloudModels`, and the
|
|
327
|
-
* collapse it is composed with) in `client/brain-catalog.js`, shared with the
|
|
328
|
-
* CLI: two copies is how this host and the terminal previously came to offer
|
|
329
|
-
* the same account two different model lists. Resolved the two ways this repo
|
|
330
|
-
* resolves every shared module — repo-relative in a checkout, and this app's
|
|
331
|
-
* staged vendor/ tree.
|
|
396
|
+
* billed to this account (see the byok branch below).
|
|
332
397
|
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
398
|
+
* The list itself is one shared rule in `client/brain-catalog.js`, shared with
|
|
399
|
+
* the CLI: two copies is how this host and the terminal previously came to
|
|
400
|
+
* offer the same account two different model lists. Resolved the two ways this
|
|
401
|
+
* repo resolves every shared module — repo-relative in a checkout, and this
|
|
402
|
+
* app's staged vendor/ tree.
|
|
337
403
|
*/
|
|
338
|
-
const {
|
|
404
|
+
const { offerableCatalog } = requireSharedBrain();
|
|
339
405
|
|
|
340
406
|
/**
|
|
341
407
|
* The BYOK additions, applied to the server's provider catalog below. Same
|
|
@@ -744,7 +810,7 @@ function createLocalEngine({
|
|
|
744
810
|
// direction too: a path wrongly claimed stops being reported as foreign.
|
|
745
811
|
const result = await T.executeTool(name, args, toolCtx);
|
|
746
812
|
if (guard && (name === 'writeFile' || name === 'editFile') && result && result.ok !== false) {
|
|
747
|
-
recordWrite(guard, args && args.
|
|
813
|
+
recordWrite(guard, args && args.file_path);
|
|
748
814
|
}
|
|
749
815
|
return result;
|
|
750
816
|
}
|
|
@@ -793,7 +859,7 @@ function createLocalEngine({
|
|
|
793
859
|
// unconditionally, making every line below it unreachable — the
|
|
794
860
|
// approved-write path recorded nothing at all.)
|
|
795
861
|
if (res && res.ok !== false && toolCtx && toolCtx.guard) {
|
|
796
|
-
recordWrite(toolCtx.guard, args && args.
|
|
862
|
+
recordWrite(toolCtx.guard, args && args.file_path);
|
|
797
863
|
}
|
|
798
864
|
return res;
|
|
799
865
|
}
|
|
@@ -855,20 +921,16 @@ function createLocalEngine({
|
|
|
855
921
|
// entry so the class is usable the moment a key lands.
|
|
856
922
|
if (!aegis.apiKey) return { class: cls, models: [], needsKey: true };
|
|
857
923
|
const data = await aegis.listModels();
|
|
858
|
-
//
|
|
859
|
-
//
|
|
860
|
-
// emptied, which is why `needsKey` keeps its meaning and the renderer's
|
|
861
|
-
// hint for this class is unchanged.
|
|
924
|
+
// The whole catalog, aliases folded — see the rule above. What the list
|
|
925
|
+
// contains does not touch `needsKey` or the renderer's per-class hint.
|
|
862
926
|
return {
|
|
863
927
|
class: cls,
|
|
864
|
-
//
|
|
865
|
-
//
|
|
866
|
-
//
|
|
867
|
-
//
|
|
868
|
-
//
|
|
869
|
-
|
|
870
|
-
// case the rule exists to catch.
|
|
871
|
-
models: filterAegisCatalog(omitAegisCloudModels(normalizeCatalog(data && data.models))),
|
|
928
|
+
// One call, because the folding has to see the payload's own `alias_of`
|
|
929
|
+
// bookkeeping: a pre-collapsed row IS the selection, so a renderer must
|
|
930
|
+
// not filter it back out as a hidden alias, and folding what has already
|
|
931
|
+
// been folded would leave a renamed tier (`nexus-brain-v2`, an alias OF
|
|
932
|
+
// the brain under a spelling this release has never seen) on screen.
|
|
933
|
+
models: offerableCatalog(normalizeCatalog(data && data.models)),
|
|
872
934
|
};
|
|
873
935
|
}
|
|
874
936
|
if (cls === 'byok') {
|
|
@@ -1153,6 +1215,12 @@ function createLocalEngine({
|
|
|
1153
1215
|
// automatically by byokChatCompletion() as X-AEGIS-Key so the account
|
|
1154
1216
|
// gets billed the handling fee; see client/aegis.js.
|
|
1155
1217
|
const { provider, model: bareModel } = splitByokModel(opts.model);
|
|
1218
|
+
// Translate the rung for THIS vendor once, and carry the verdict rather
|
|
1219
|
+
// than a bare level: the relay validates strictly upstream and the field
|
|
1220
|
+
// name is not portable (DeepSeek's OpenAI-compatible surface takes
|
|
1221
|
+
// `reasoning_effort`, same spelling as OpenAI's). Undefined for a family
|
|
1222
|
+
// with no dialect, which the transport then omits entirely.
|
|
1223
|
+
const effort = nativeEffort(provider, bareModel, opts.effort);
|
|
1156
1224
|
try {
|
|
1157
1225
|
return await aegis.byokChatCompletion({
|
|
1158
1226
|
provider,
|
|
@@ -1162,6 +1230,13 @@ function createLocalEngine({
|
|
|
1162
1230
|
system: opts.system,
|
|
1163
1231
|
messages: opts.messages,
|
|
1164
1232
|
maxTokens: opts.maxTokens,
|
|
1233
|
+
// Native effort knob, field and level both from the dialect table.
|
|
1234
|
+
// `bareModel` is already provider-less, so it matches the dialect
|
|
1235
|
+
// regexes without further stripping. Only a dialect whose field is
|
|
1236
|
+
// `reasoning_effort` is forwarded: DeepSeek used to ride a separate
|
|
1237
|
+
// `effort` body key, which the API 200s and ignores, so that whole
|
|
1238
|
+
// second channel is gone rather than left as a way to reintroduce it.
|
|
1239
|
+
reasoningEffort: effort && effort.field === 'reasoning_effort' ? effort.level : undefined,
|
|
1165
1240
|
stream: true,
|
|
1166
1241
|
onStream: opts.onDelta,
|
|
1167
1242
|
signal: opts.signal,
|
|
@@ -1863,4 +1938,4 @@ function createLocalEngine({
|
|
|
1863
1938
|
};
|
|
1864
1939
|
}
|
|
1865
1940
|
|
|
1866
|
-
module.exports = { CLASSES, createLocalEngine, extractToolCalls, parseArgs, reasoningBudget };
|
|
1941
|
+
module.exports = { CLASSES, createLocalEngine, extractToolCalls, parseArgs, reasoningBudget, nativeEffort };
|