aegis-desktop 1.0.10 → 1.0.13
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/auto.js +11 -7
- package/lib/fingerprint.js +76 -17
- package/lib/google-connect.js +171 -0
- package/lib/local/claude-pro.js +134 -11
- package/lib/local/engine.js +111 -26
- package/lib/local/prompt.js +67 -37
- package/lib/local/tools.js +49 -13
- package/lib/settings.js +65 -24
- package/main.js +270 -36
- package/package.json +5 -2
- package/preload.js +45 -12
- package/renderer/app.js +691 -22
- package/renderer/budget.js +18 -9
- package/renderer/companion.js +225 -0
- package/renderer/fingerprint.css +16 -0
- package/renderer/fingerprint.js +136 -2
- package/renderer/index.html +302 -121
- package/renderer/provider-toolbar.js +136 -0
- package/renderer/providers.js +261 -0
- package/renderer/style.css +547 -171
- package/vendor/aegis.js +169 -3
- package/vendor/credentials.js +9 -2
- package/vendor/drive-connect.js +87 -0
- package/vendor/drive-upload.js +176 -0
- package/vendor/google-signin.js +127 -0
- package/vendor/record-vault.js +431 -113
- package/vendor/update.js +366 -8
package/lib/local/engine.js
CHANGED
|
@@ -167,6 +167,32 @@ function normalizeToolArgs(T, name, args, ctx) {
|
|
|
167
167
|
return fn(name, args, ctx);
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Is `dir` inside a git work tree? Walks up the way git itself does, so a turn
|
|
172
|
+
* running in a subdirectory of a repo is still told it is one.
|
|
173
|
+
*
|
|
174
|
+
* Returns `'git'`, `''` (definitely not a repo), or `null` when the probe could
|
|
175
|
+
* not answer — the caller omits the line rather than asserting "not a repo",
|
|
176
|
+
* because a wrong negative is exactly the kind of fact a model will act on.
|
|
177
|
+
*/
|
|
178
|
+
function detectVcs(dir) {
|
|
179
|
+
if (!dir) return null;
|
|
180
|
+
try {
|
|
181
|
+
const fs = require('node:fs');
|
|
182
|
+
const path = require('node:path');
|
|
183
|
+
let probe = path.resolve(String(dir));
|
|
184
|
+
for (let i = 0; i < 8; i += 1) {
|
|
185
|
+
if (fs.existsSync(path.join(probe, '.git'))) return 'git';
|
|
186
|
+
const up = path.dirname(probe);
|
|
187
|
+
if (up === probe) break;
|
|
188
|
+
probe = up;
|
|
189
|
+
}
|
|
190
|
+
return '';
|
|
191
|
+
} catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
170
196
|
function hostEnv(base = {}, supplied = {}, payload = null) {
|
|
171
197
|
const b = base || {};
|
|
172
198
|
const s = supplied || {};
|
|
@@ -250,6 +276,16 @@ function hostEnv(base = {}, supplied = {}, payload = null) {
|
|
|
250
276
|
// a `{ key, value }` facet, or a bare string.
|
|
251
277
|
languages: pick('languages', b.languages),
|
|
252
278
|
cwd: pick('cwd', cwd),
|
|
279
|
+
// Whether the turn's directory is a git work tree, probed once per turn.
|
|
280
|
+
// Measured reason: the work rules order the model to check prior work with
|
|
281
|
+
// `git log` before it starts, and a benchmark workspace (a temp dir holding
|
|
282
|
+
// three files) is NOT a repo. On every task the model dutifully spent its
|
|
283
|
+
// second round on `cd … && git log`, paid a failed exec, and carried the
|
|
284
|
+
// error text into every later round's prompt — one wasted round in seven,
|
|
285
|
+
// and a bigger prefix for the rest. The host already knew; this states it
|
|
286
|
+
// so the rule can say "skip the sources the workspace does not have" and
|
|
287
|
+
// mean something the model can act on without guessing.
|
|
288
|
+
vcs: pick('vcs', b.vcs != null ? b.vcs : detectVcs(cwd)),
|
|
253
289
|
shell: pick('shell', shellName),
|
|
254
290
|
shellKind: pick('shellKind', shellKind),
|
|
255
291
|
roots: pick('roots', b.roots),
|
|
@@ -524,27 +560,43 @@ const EFFORT_TOKEN_BUDGET = { low: 8192, medium: 16384, high: 32768 };
|
|
|
524
560
|
/**
|
|
525
561
|
* DeepSeek native reasoning level per config rung.
|
|
526
562
|
*
|
|
527
|
-
* The field is `
|
|
528
|
-
*
|
|
529
|
-
* `
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
563
|
+
* The field is `effort`, NOT `reasoning_effort`. Verified live 2026-10-21
|
|
564
|
+
* against the surface this client actually reaches — the relay's
|
|
565
|
+
* `https://api.deepseek.com/anthropic` transport — by measuring output tokens
|
|
566
|
+
* over interleaved paired samples (n=12) against an absent-field baseline:
|
|
567
|
+
*
|
|
568
|
+
* absent 818 · low 772 · medium 836 · high 818 · max 1494
|
|
569
|
+
*
|
|
570
|
+
* Only `max` separates from sending nothing. `GET /models` reports
|
|
571
|
+
* `supported_levels: ["low","high","max"]`, default `high`, on both live ids
|
|
572
|
+
* (`deepseek-flash`, `deepseek-v4-pro`) — so `medium` is not a level these
|
|
573
|
+
* models have. It passes the deserializer (unknown values *do* 422 with the
|
|
574
|
+
* enum, so validation is real) and is then absorbed into the default, which is
|
|
575
|
+
* this codebase's worst failure mode: a 200, nothing surfaced, and the model
|
|
576
|
+
* thinking at a depth nobody chose.
|
|
577
|
+
*
|
|
578
|
+
* What the earlier version of this comment asserted — that `reasoning_effort`
|
|
579
|
+
* binds and `effort` is ignored — was measured on the vendor's OpenAI-shaped
|
|
580
|
+
* `/chat/completions`, a surface this client never reaches, and it led to the
|
|
581
|
+
* knob being dead end-to-end: aegis1 forwards `reasoning_effort` only for
|
|
582
|
+
* BYOK_REASONING_EFFORT_PROVIDERS (openai/groq/gemini/grok — deepseek is
|
|
583
|
+
* absent), so the field was dropped at the relay. The `effort` channel is the
|
|
584
|
+
* one aegis1 gates on `provider_id == "deepseek"` (app.py) and that the
|
|
585
|
+
* Anthropic adapter nests as `output_config.effort`; it is the only channel
|
|
586
|
+
* that reaches the vendor, and the only one that measurably binds.
|
|
536
587
|
*
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
*
|
|
540
|
-
*
|
|
541
|
-
*
|
|
588
|
+
* The map folds `medium` upward for the same reason the Gemini row does: the
|
|
589
|
+
* vendor has no such level, and a rung that 200s while doing nothing is worse
|
|
590
|
+
* than one that says what it does. `max` is deliberately NOT reachable — it is
|
|
591
|
+
* not a rung of this knob, and mapping the default rung onto it would silently
|
|
592
|
+
* multiply reasoning spend on every turn nobody asked to deepen.
|
|
593
|
+
* `max_tokens` (EFFORT_TOKEN_BUDGET) still scales as the output ceiling, since
|
|
594
|
+
* hidden chain-of-thought shares that budget on DeepSeek.
|
|
542
595
|
*
|
|
543
|
-
*
|
|
544
|
-
*
|
|
545
|
-
* no-drift guarantee as DEEPSEEK_REASONING_MODEL_RE above.
|
|
596
|
+
* desktop/renderer/budget.js carries the renderer's copy; test/budget.test.mjs
|
|
597
|
+
* compares the two tables, so the mirror cannot drift silently.
|
|
546
598
|
*/
|
|
547
|
-
const DEEPSEEK_EFFORT = { low: 'low', medium: '
|
|
599
|
+
const DEEPSEEK_EFFORT = { low: 'low', medium: 'high', high: 'high' };
|
|
548
600
|
|
|
549
601
|
/**
|
|
550
602
|
* Per-family effort translation, mirroring the upstream engine's
|
|
@@ -565,7 +617,7 @@ const DEEPSEEK_EFFORT = { low: 'low', medium: 'medium', high: 'high' };
|
|
|
565
617
|
const EFFORT_IDENTITY = { low: 'low', medium: 'medium', high: 'high' };
|
|
566
618
|
const EFFORT_INERT_RE = /non-reasoning/i;
|
|
567
619
|
const EFFORT_DIALECTS = [
|
|
568
|
-
{ provider: 'deepseek', re: DEEPSEEK_REASONING_MODEL_RE, field: '
|
|
620
|
+
{ provider: 'deepseek', re: DEEPSEEK_REASONING_MODEL_RE, field: 'effort', map: DEEPSEEK_EFFORT },
|
|
569
621
|
{ provider: 'openai', re: /^(gpt-5|o[0-9])/i, field: 'reasoning_effort', map: EFFORT_IDENTITY },
|
|
570
622
|
{ provider: 'groq', re: /^(openai\/gpt-oss|qwen\/qwen3)/i, field: 'reasoning_effort', map: EFFORT_IDENTITY },
|
|
571
623
|
{ provider: 'xai', re: /^grok-(4\.[5-9]|[5-9])/, field: 'reasoning_effort', map: EFFORT_IDENTITY },
|
|
@@ -1967,8 +2019,8 @@ function createLocalEngine({
|
|
|
1967
2019
|
const { provider, model: bareModel } = splitByokModel(opts.model);
|
|
1968
2020
|
// Translate the rung for THIS vendor once, and carry the verdict rather
|
|
1969
2021
|
// than a bare level: the relay validates strictly upstream and the field
|
|
1970
|
-
// name is not portable (DeepSeek's
|
|
1971
|
-
//
|
|
2022
|
+
// name is not portable (DeepSeek's `/anthropic` transport takes `effort`,
|
|
2023
|
+
// OpenAI-shaped vendors take `reasoning_effort`). Undefined for a family
|
|
1972
2024
|
// with no dialect, which the transport then omits entirely.
|
|
1973
2025
|
const effort = nativeEffort(provider, bareModel, opts.effort);
|
|
1974
2026
|
try {
|
|
@@ -1999,10 +2051,16 @@ function createLocalEngine({
|
|
|
1999
2051
|
toolChoice: opts.toolChoice,
|
|
2000
2052
|
// Native effort knob, field and level both from the dialect table.
|
|
2001
2053
|
// `bareModel` is already provider-less, so it matches the dialect
|
|
2002
|
-
// regexes without further stripping.
|
|
2003
|
-
//
|
|
2004
|
-
// `
|
|
2005
|
-
//
|
|
2054
|
+
// regexes without further stripping. Both wire channels are live, and
|
|
2055
|
+
// which one is used is the dialect's decision, not this call site's:
|
|
2056
|
+
// DeepSeek's `/anthropic` transport takes `effort` (aegis1 gates it on
|
|
2057
|
+
// provider_id == "deepseek" and the adapter nests it as
|
|
2058
|
+
// `output_config.effort`), while the OpenAI-shaped vendors take
|
|
2059
|
+
// `reasoning_effort`. Routing on the field name is what keeps a
|
|
2060
|
+
// dialect from being silently dropped here — the previous form
|
|
2061
|
+
// forwarded only `reasoning_effort`, so turning DeepSeek's dialect
|
|
2062
|
+
// over to `effort` would have died at this line.
|
|
2063
|
+
effort: effort && effort.field === 'effort' ? effort.level : undefined,
|
|
2006
2064
|
reasoningEffort: effort && effort.field === 'reasoning_effort' ? effort.level : undefined,
|
|
2007
2065
|
stream: true,
|
|
2008
2066
|
onStream: opts.onDelta,
|
|
@@ -2541,7 +2599,34 @@ function createLocalEngine({
|
|
|
2541
2599
|
// dispatch is summed, including the truncation retry and the synthesis
|
|
2542
2600
|
// re-dispatch: both are real, separately billed provider calls.
|
|
2543
2601
|
const turnUsage = { calls: 0 };
|
|
2544
|
-
|
|
2602
|
+
// The cache fields MUST be listed here, not just summed at the top level.
|
|
2603
|
+
// DeepSeek reports its hit count as `prompt_cache_hit_tokens` and its
|
|
2604
|
+
// miss count as `prompt_cache_miss_tokens`, and neither name was in this
|
|
2605
|
+
// list — so every tool-using turn dropped them on the way out and handed
|
|
2606
|
+
// its caller a usage object with no cache information at all. The CLI's
|
|
2607
|
+
// `--json` republishes exactly this accumulator, `usageBuckets` then had
|
|
2608
|
+
// nothing to read, and the published cost of an agentic run was priced
|
|
2609
|
+
// entirely at the miss rate. Measured on the 3-task sweep: `cache_read: 0`
|
|
2610
|
+
// on all three trials (tokens_in 210,904) while the row's own baseline
|
|
2611
|
+
// showed 72,448 cached tokens. The same defect the CLI's per-turn readout
|
|
2612
|
+
// was fixed for, here one layer below it.
|
|
2613
|
+
const USAGE_FIELDS = [
|
|
2614
|
+
'input_tokens', 'output_tokens', 'prompt_tokens', 'completion_tokens', 'total_tokens',
|
|
2615
|
+
// Anthropic spelling (disjoint from its input count).
|
|
2616
|
+
'cache_read_input_tokens', 'cache_creation_input_tokens',
|
|
2617
|
+
// DeepSeek/OpenAI spelling (both SUBSETs of the prompt count, which is
|
|
2618
|
+
// what `usageBuckets` detects and subtracts rather than double-billing).
|
|
2619
|
+
'prompt_cache_hit_tokens', 'prompt_cache_miss_tokens',
|
|
2620
|
+
// Hidden deliberation, which every provider here bills as OUTPUT but
|
|
2621
|
+
// none of them split out for free. These were absent from the list
|
|
2622
|
+
// while `reasoningTokensOf` (below) already knew how to read them, so
|
|
2623
|
+
// the one place that could have separated "wrote more code" from
|
|
2624
|
+
// "thought longer" threw the split away at the accumulator and handed
|
|
2625
|
+
// its caller a single `output_tokens` with CoT folded in. A benchmark
|
|
2626
|
+
// row could therefore see that a turn cost 1.9x another's output and
|
|
2627
|
+
// still have no way to say which half moved.
|
|
2628
|
+
'reasoning_tokens', 'thinking_tokens',
|
|
2629
|
+
];
|
|
2545
2630
|
const addUsage = (res) => {
|
|
2546
2631
|
const u = res && res.usage;
|
|
2547
2632
|
if (!u || typeof u !== 'object') return;
|
package/lib/local/prompt.js
CHANGED
|
@@ -30,50 +30,62 @@
|
|
|
30
30
|
|
|
31
31
|
/** Identity + work rules. Ported from the upstream engine reference. */
|
|
32
32
|
const MAIN_CHAT_PROMPT =
|
|
33
|
-
`You are Aegiscodex, a terminal coding assistant
|
|
34
|
-
`
|
|
35
|
-
|
|
33
|
+
`You are Aegiscodex, a terminal coding assistant working in the user's repository: read and ` +
|
|
34
|
+
`reason about code, write and edit files, run shell commands, investigate bugs. Work rules:\n` +
|
|
35
|
+
`- Output is billed by the token and is the largest line on the bill, so spend it where it is ` +
|
|
36
|
+
` read. Before a tool call, one short sentence on what you are about to find out. Do NOT ` +
|
|
37
|
+
` restate file contents, diffs, command output or test results the user can already see. ` +
|
|
38
|
+
` Finish with the answer in a few lines — expand only when asked to explain, or when the ` +
|
|
39
|
+
` change's correctness depends on a detail the user must check.\n` +
|
|
40
|
+
`- Batch independent tool calls into ONE turn: read several files, or grep for several ` +
|
|
41
|
+
` patterns, at once, or use one exec command that does the whole job. Every round ` +
|
|
42
|
+
` re-sends the prefix and bills fresh output; one call per round is the expensive shape.\n` +
|
|
36
43
|
`- Use tools silently. A one-line reason is enough; do not narrate your plan as a story. ` +
|
|
37
44
|
`- Never claim what a tool found or what a command returned before the tool actually runs. ` +
|
|
38
45
|
` Report only the results you really received. ` +
|
|
39
|
-
`- Before
|
|
40
|
-
` it
|
|
41
|
-
`
|
|
42
|
-
`
|
|
43
|
-
`
|
|
44
|
-
`
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
`
|
|
48
|
-
`
|
|
46
|
+
`- Before starting a fix or feature, check whether they have asked for it before and whether ` +
|
|
47
|
+
` it was delivered: recalled memory, \`queue.jsonl\`, \`PLAN.md\`/notes and \`git log\` — ` +
|
|
48
|
+
` ONE batched command over sources that exist: if the Environment block says this ` +
|
|
49
|
+
` directory has no version control, skip it. ` +
|
|
50
|
+
` If it was resolved before and the problem is back, report how it was resolved and ` +
|
|
51
|
+
` why it regressed, then fix the cause — never re-fix blind as if it were new. ` +
|
|
52
|
+
` If it was never met, say so plainly before starting. ` +
|
|
53
|
+
`- Match the response to the request. A greeting, a question you already know the answer to, ` +
|
|
54
|
+
` or a request for an opinion is answered in plain words with NO tools. Reach for a tool ` +
|
|
55
|
+
` only when the answer depends on something in this repo or on this machine. ` +
|
|
56
|
+
` "hey" is not a task.\n` +
|
|
49
57
|
`- WHEN THE USER HAS ASKED FOR WORK: act, don't just inspect. After at most 2 rounds of ` +
|
|
50
58
|
` reading or exploration, start making changes with writeFile or editFile. Reconnaissance ` +
|
|
51
|
-
` is not progress — implement, then verify. This
|
|
52
|
-
`
|
|
59
|
+
` is not progress — implement, then verify. This is about HOW to carry out a task you were ` +
|
|
60
|
+
` given; it is never a reason to invent one. Never write or modify a file the user ` +
|
|
53
61
|
` did not ask you to touch.\n` +
|
|
54
|
-
`- Zero tolerance for stalling: once the
|
|
55
|
-
`
|
|
56
|
-
`
|
|
57
|
-
`
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
` user's question. Never end your turn with an intention like "Let me check…" or "I'll now…" ` +
|
|
61
|
-
` — that is not an answer. ` +
|
|
62
|
+
`- Zero tolerance for stalling: once the cause is identified and only the edit remains, make ` +
|
|
63
|
+
` it in the same turn. Never end a turn with an empty or no-op response, or a "let me ` +
|
|
64
|
+
` continue"/"I'll now…" placeholder when the user says continue — that wastes their turn. ` +
|
|
65
|
+
` This has been flagged as non-compliance before; treat it as a hard rule. ` +
|
|
66
|
+
`- When you have what you need, stop using tools and give a concise, direct answer. Never ` +
|
|
67
|
+
` end a turn with an intention like "Let me check…" — that is not an answer. ` +
|
|
62
68
|
`- When a focused multi-step sub-task can be delegated, use the task tool to spawn a ` +
|
|
63
|
-
` specialist subagent rather than doing
|
|
69
|
+
` specialist subagent rather than doing it inline. ` +
|
|
64
70
|
`- If the user references a workflow you don't recognize, inspect the repo/scripts for that ` +
|
|
65
|
-
` mechanism before acting
|
|
71
|
+
` mechanism before acting.`;
|
|
66
72
|
|
|
67
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* The tool line: what each tool is, and the two facts a schema cannot carry.
|
|
75
|
+
*
|
|
76
|
+
* It used to restate the path rule in full — absolute, `~` expansion, relative
|
|
77
|
+
* resolution — which `PATH_ARG_DESC` already says once per path argument in nine
|
|
78
|
+
* schemas. That duplication is what `d01068b` unwound from the schemas; this is
|
|
79
|
+
* the other end of it. What stays is the part no schema carries: the
|
|
80
|
+
* platform-specific example (the one form a model gets wrong) and the persistent
|
|
81
|
+
* shell, which is a property of the session rather than an argument.
|
|
82
|
+
*/
|
|
68
83
|
const TOOL_LINE =
|
|
69
|
-
'Tools: readFile, writeFile, editFile, listDir, glob, grep, exec, task. Paths are absolute
|
|
70
|
-
'
|
|
71
|
-
'
|
|
72
|
-
'
|
|
73
|
-
'
|
|
74
|
-
'exec runs in a persistent shell session on this machine — cd and exported env vars carry ' +
|
|
75
|
-
'across calls within the turn, like a real terminal. task spawns a specialist subagent ' +
|
|
76
|
-
'(its own tool loop, same model) for a focused, self-contained piece of work.';
|
|
84
|
+
'Tools: readFile, writeFile, editFile, listDir, glob, grep, exec, task. Paths are absolute ' +
|
|
85
|
+
'in the form THIS machine uses (see the Environment block): on win32 that means ' +
|
|
86
|
+
'C:\\Users\\you\\Desktop\\file.txt, not /home/you/Desktop/file.txt or /tmp/file.txt. ' +
|
|
87
|
+
'exec runs in a persistent shell session — cd and exported env vars carry across calls ' +
|
|
88
|
+
'within the turn. task spawns a specialist subagent (its own tool loop, same model).';
|
|
77
89
|
|
|
78
90
|
/**
|
|
79
91
|
* One durable-language entry as a sentence fragment, accepting the three shapes
|
|
@@ -109,6 +121,7 @@ function environmentPreamble(env = {}) {
|
|
|
109
121
|
localeName,
|
|
110
122
|
languages,
|
|
111
123
|
cwd,
|
|
124
|
+
vcs,
|
|
112
125
|
shell,
|
|
113
126
|
shellKind,
|
|
114
127
|
roots,
|
|
@@ -164,8 +177,7 @@ function environmentPreamble(env = {}) {
|
|
|
164
177
|
if (desktopdir) {
|
|
165
178
|
bits.push(
|
|
166
179
|
`desktop directory: ${desktopdir} — use this exact path when the user says "my desktop"; ` +
|
|
167
|
-
'do NOT build it yourself from the home directory,
|
|
168
|
-
'called "Desktop" there',
|
|
180
|
+
'do NOT build it yourself from the home directory',
|
|
169
181
|
);
|
|
170
182
|
}
|
|
171
183
|
// Same failure mode as the desktop line, one directory over, and reported
|
|
@@ -176,13 +188,31 @@ function environmentPreamble(env = {}) {
|
|
|
176
188
|
// missing path cannot tell a typo from an empty directory.
|
|
177
189
|
if (dataDir) {
|
|
178
190
|
bits.push(
|
|
179
|
-
`AEGIS data directory: ${dataDir} —
|
|
191
|
+
`AEGIS data directory: ${dataDir} — the store itself (config, sessions, ` +
|
|
180
192
|
'credentials, queue). Use this exact path; do NOT append it to the home directory ' +
|
|
181
193
|
'yourself, because a dropped separator ("C:\\Users\\you.aegiscode") names a path that ' +
|
|
182
194
|
'does not exist and looks like an empty directory rather than a mistake',
|
|
183
195
|
);
|
|
184
196
|
}
|
|
185
197
|
if (cwd) bits.push(`working directory: ${cwd}`);
|
|
198
|
+
// The version-control fact, which turns the work rules' "check whether this
|
|
199
|
+
// was already delivered" from a guess into a decision. Measured: in a temp
|
|
200
|
+
// workspace with no `.git`, the model still ran `git log` on its second round
|
|
201
|
+
// of all three tasks, spent a round on a guaranteed failure, and carried the
|
|
202
|
+
// error into the rest of the turn. Stated only when the host actually probed
|
|
203
|
+
// (`null`/`undefined` means "could not tell", and silence beats a wrong fact).
|
|
204
|
+
if (vcs === 'git') {
|
|
205
|
+
bits.push(
|
|
206
|
+
`version control: this directory IS inside a git work tree, so \`git log\`, ` +
|
|
207
|
+
'`git diff` and `git status` are usable for checking what was already done',
|
|
208
|
+
);
|
|
209
|
+
} else if (vcs === '') {
|
|
210
|
+
bits.push(
|
|
211
|
+
`version control: this directory is NOT a git repository and has no history to ` +
|
|
212
|
+
'consult — do NOT run `git log`, `git diff`, `git blame` or `git status`; they ' +
|
|
213
|
+
'fail here and cost a round. Read the files that exist instead',
|
|
214
|
+
);
|
|
215
|
+
}
|
|
186
216
|
if (shell) bits.push(`exec shell: ${shell}${shellKind ? ` (${shellKind})` : ''}`);
|
|
187
217
|
if (model) bits.push(`model: ${model}`);
|
|
188
218
|
if (appVersion) bits.push(`AEGIS Desktop ${appVersion}`);
|
package/lib/local/tools.js
CHANGED
|
@@ -86,10 +86,18 @@ const OUTPUT_CAP = 30_000; // chars fed back to the model per tool result
|
|
|
86
86
|
* Desktop. The schema is the first place to say so — a refusal after the fact
|
|
87
87
|
* costs the model a round, a correct description costs it nothing.
|
|
88
88
|
*/
|
|
89
|
+
// Kept SHORT on purpose. This sentence is repeated in the description of every
|
|
90
|
+
// path argument (9 of the 10 tools), so its length is paid once per tool, every
|
|
91
|
+
// round, and the tool schemas are part of the wire prefix the provider is asked
|
|
92
|
+
// to cache. The long form spelled out win32 examples and the Environment-block
|
|
93
|
+
// cross-reference — 267 chars, ~68 tokens, ~616 tokens per round once
|
|
94
|
+
// multiplied across the nine. That instruction is NOT lost: prompt.js's
|
|
95
|
+
// TOOL_LINE states the same rule once, in the system prompt where it belongs
|
|
96
|
+
// and where it costs one copy instead of nine. `test/tool-paths.test.mjs`
|
|
97
|
+
// asserts this constant appears in each path argument, so it must stay the
|
|
98
|
+
// single shared string rather than being inlined per tool.
|
|
89
99
|
const PATH_ARG_DESC =
|
|
90
|
-
'Absolute path
|
|
91
|
-
'directory — a win32 host needs C:\\Users\\you\\file.txt, never /home/you/file.txt), or "~" / ' +
|
|
92
|
-
'"~/…" for the home directory. A relative path resolves against the working directory.';
|
|
100
|
+
'Absolute path on THIS machine; "~"/"~/…" = home. Relative resolves against the working directory.';
|
|
93
101
|
|
|
94
102
|
/** `<what>. <the path rules>` — the shape every path argument's description takes. */
|
|
95
103
|
const pathDesc = (what) => `${what}. ${PATH_ARG_DESC}`;
|
|
@@ -297,18 +305,45 @@ const SCHEMAS = {
|
|
|
297
305
|
// MAX_SUBAGENT_DEPTH, hard-bounding runaway recursion.
|
|
298
306
|
const SUBAGENT_TOOL = 'task';
|
|
299
307
|
|
|
308
|
+
/**
|
|
309
|
+
* The hook layer (`hooks`, `installHooks`) is the ONE part of this registry that
|
|
310
|
+
* is not agent work, and it is the one part the benchmark never touches.
|
|
311
|
+
*
|
|
312
|
+
* Measured on `benchmarks/raw/beat-pi-20261022-2318` (three tasks, the same wire
|
|
313
|
+
* model, nine trials): the advertised schemas are re-sent as a wire PREFIX on
|
|
314
|
+
* every round, and these two are 427 of its ~1,757 tokens. On the round that
|
|
315
|
+
* misses the prefix cache they are billed at the miss rate; on every later round
|
|
316
|
+
* they are billed at the cheap hit rate but are still tokens the model reads
|
|
317
|
+
* before it reads the task. Pi ships seven tools and no hook surface at all.
|
|
318
|
+
*
|
|
319
|
+
* So they stay RUNNABLE (`isTool`, `executeTool`, `toolList`, the CLI's `/hooks`)
|
|
320
|
+
* and stay on the schema list for a caller that asks for them, but they are no
|
|
321
|
+
* longer advertised to the model by default. Capability is unchanged; the
|
|
322
|
+
* prefix it was paying for is gone.
|
|
323
|
+
*/
|
|
324
|
+
const HOOK_TOOLS = new Set(['hooks', 'installHooks']);
|
|
325
|
+
|
|
300
326
|
/** Tool names in advertisement order. `includeSubagent: false` drops task (depth cap). */
|
|
301
|
-
function toolNames({ includeSubagent = true } = {}) {
|
|
302
|
-
return schemaList(includeSubagent).map((s) => s.name);
|
|
327
|
+
function toolNames({ includeSubagent = true, includeHooks = false } = {}) {
|
|
328
|
+
return schemaList({ includeSubagent, includeHooks }).map((s) => s.name);
|
|
303
329
|
}
|
|
304
330
|
|
|
305
|
-
function schemaList(includeSubagent) {
|
|
306
|
-
return Object.values(SCHEMAS).filter(
|
|
331
|
+
function schemaList({ includeSubagent = true, includeHooks = false } = {}) {
|
|
332
|
+
return Object.values(SCHEMAS).filter(
|
|
333
|
+
(s) =>
|
|
334
|
+
(includeSubagent || s.name !== SUBAGENT_TOOL) &&
|
|
335
|
+
(includeHooks || !HOOK_TOOLS.has(s.name))
|
|
336
|
+
);
|
|
307
337
|
}
|
|
308
338
|
|
|
309
|
-
/**
|
|
310
|
-
|
|
311
|
-
|
|
339
|
+
/**
|
|
340
|
+
* OpenAI-compatible /chat/completions tool definitions ({type:'function', function}).
|
|
341
|
+
* `includeHooks: true` re-advertises the hook tools; the default is the agent
|
|
342
|
+
* set, because a hook install is a thing the USER asks the CLI for, not a step
|
|
343
|
+
* in a coding task.
|
|
344
|
+
*/
|
|
345
|
+
function openaiTools({ includeSubagent = true, includeHooks = false } = {}) {
|
|
346
|
+
return schemaList({ includeSubagent, includeHooks }).map((s) => ({
|
|
312
347
|
type: 'function',
|
|
313
348
|
function: { name: s.name, description: s.description, parameters: s.parameters },
|
|
314
349
|
}));
|
|
@@ -319,10 +354,10 @@ function openaiTools({ includeSubagent = true } = {}) {
|
|
|
319
354
|
* goes through the AEGIS relay, which speaks the OpenAI shape — so this takes
|
|
320
355
|
* no `wire` argument. The Anthropic-shaped variant went away with the
|
|
321
356
|
* direct-dial transport. `includeSubagent: false` drops the task tool
|
|
322
|
-
* (subagent depth cap).
|
|
357
|
+
* (subagent depth cap); `includeHooks: true` re-adds hooks/installHooks.
|
|
323
358
|
*/
|
|
324
|
-
function toolsFor({ includeSubagent = true } = {}) {
|
|
325
|
-
return openaiTools({ includeSubagent });
|
|
359
|
+
function toolsFor({ includeSubagent = true, includeHooks = false } = {}) {
|
|
360
|
+
return openaiTools({ includeSubagent, includeHooks });
|
|
326
361
|
}
|
|
327
362
|
|
|
328
363
|
// ── Path normalisation ──────────────────────────────────────────────────────
|
|
@@ -1175,6 +1210,7 @@ module.exports = {
|
|
|
1175
1210
|
// schemas
|
|
1176
1211
|
SCHEMAS,
|
|
1177
1212
|
SUBAGENT_TOOL,
|
|
1213
|
+
HOOK_TOOLS,
|
|
1178
1214
|
toolNames,
|
|
1179
1215
|
openaiTools,
|
|
1180
1216
|
toolsFor,
|
package/lib/settings.js
CHANGED
|
@@ -103,6 +103,19 @@ const QUICK_LAUNCHER_NAMESPACE = '__quickLauncher';
|
|
|
103
103
|
* fighting existing app/OS bindings, memorable enough to type once. */
|
|
104
104
|
const DEFAULT_QUICK_LAUNCHER_SHORTCUT = 'CmdOrCtrl+Shift+Space';
|
|
105
105
|
|
|
106
|
+
/** Reserved namespace for where `fingerprint publish`/the vault card sends
|
|
107
|
+
* sealed records: `{ backend }`, one of VAULT_BACKEND_VALUES — app-level, not
|
|
108
|
+
* a provider, for the same reason the quick launcher isn't. Default is
|
|
109
|
+
* `aegiscloud` (unset reads as the default, same convention as confirm mode)
|
|
110
|
+
* because it is the only backend that exists on a fresh install — see
|
|
111
|
+
* client/record-vault.js's own DEFAULT_BACKEND. `drive` asks the fingerprint
|
|
112
|
+
* service to talk to rclone's `gdrive` remote instead (the user's own Drive,
|
|
113
|
+
* configured by them); `off` means publish/anchor/vault/audit refuse with a
|
|
114
|
+
* named reason rather than silently reaching either backend. */
|
|
115
|
+
const VAULT_BACKEND_NAMESPACE = '__vaultBackend';
|
|
116
|
+
const DEFAULT_VAULT_BACKEND = 'aegiscloud';
|
|
117
|
+
const VAULT_BACKEND_VALUES = Object.freeze(['aegiscloud', 'drive', 'off']);
|
|
118
|
+
|
|
106
119
|
/** Reserved namespace for the tool-call approval ("confirm mode") preference:
|
|
107
120
|
* `{ enabled }` — app-level, not a provider, so it stays out of the provider
|
|
108
121
|
* CRUD surface for the same reason the AEGIS key does. Default is ON
|
|
@@ -138,31 +151,34 @@ const AVATAR_NAMESPACE = '__avatar';
|
|
|
138
151
|
* main.js telemetryEnabled() through getTelemetryOptIn(). */
|
|
139
152
|
const TELEMETRY_NAMESPACE = '__telemetry';
|
|
140
153
|
|
|
141
|
-
/** Reserved namespace for the auto-drain
|
|
154
|
+
/** Reserved namespace for the auto-drain switch:
|
|
142
155
|
* `{ enabled, intervalMs }` — app-level, not a provider.
|
|
143
156
|
*
|
|
144
|
-
* DEFAULT
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
* `__confirmMode`/`__memoryPersist` above. */
|
|
157
|
+
* DEFAULT ON (user request: the queue must work with no click and nothing on
|
|
158
|
+
* screen). An absent namespace means "drain in the background"; only an
|
|
159
|
+
* explicit `enabled: false` disarms it, and that is what the settings store
|
|
160
|
+
* writes when someone turns the worker off from the CLI or by hand.
|
|
161
|
+
*
|
|
162
|
+
* The guards are what make that default defensible, and they live in
|
|
163
|
+
* `lib/auto.js`, not here: a tick skips when the queue is empty (so an idle
|
|
164
|
+
* install costs one JSON parse and no lock I/O), when another host holds the
|
|
165
|
+
* drain lock, when a drain is already in flight, and when the switch was
|
|
166
|
+
* turned off since the last tick — the switch is re-read on EVERY tick, so
|
|
167
|
+
* disarming stops the very next one. Nothing secret lives here, so no
|
|
168
|
+
* encryption — same shape as `__confirmMode`/`__memoryPersist` above. */
|
|
157
169
|
const AUTO_DRAIN_NAMESPACE = '__autoDrain';
|
|
158
170
|
|
|
159
|
-
/** Reserved namespace for the auto-mint (seal-on-quiet)
|
|
171
|
+
/** Reserved namespace for the auto-mint (seal-on-quiet) switch:
|
|
160
172
|
* `{ enabled, publish }` — app-level, not a provider.
|
|
161
173
|
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
174
|
+
* `enabled` is DEFAULT ON, same as auto-drain: a session seals itself once it
|
|
175
|
+
* has been quiet, with nothing on screen asking for it. Only an explicit
|
|
176
|
+
* `enabled: false` disarms it.
|
|
177
|
+
*
|
|
178
|
+
* `publish` stays a separate, DEFAULT OFF switch: sealing writes a local
|
|
179
|
+
* record, whereas publishing sends those bytes to the vault over `rclone` and
|
|
180
|
+
* writes an anchor beside them. An automatic network copy is never implied by
|
|
181
|
+
* an automatic seal.
|
|
166
182
|
*
|
|
167
183
|
* The AI half of the record resolves through the shared rungs in
|
|
168
184
|
* client/attest.js (`$AEGIS_AI_SECRET`/`$AEGIS_AI_MODEL` in the app scope) —
|
|
@@ -195,6 +211,7 @@ const RESERVED_NAMESPACES = Object.freeze([
|
|
|
195
211
|
AEGIS_KEY_NAMESPACE,
|
|
196
212
|
LEGACY_AEGIS_NAMESPACE,
|
|
197
213
|
QUICK_LAUNCHER_NAMESPACE,
|
|
214
|
+
VAULT_BACKEND_NAMESPACE,
|
|
198
215
|
CONFIRM_MODE_NAMESPACE,
|
|
199
216
|
MEMORY_PERSIST_NAMESPACE,
|
|
200
217
|
AVATAR_NAMESPACE,
|
|
@@ -539,6 +556,24 @@ function createSettingsStore({ dir, safeStorage, prefsDir, env } = {}) {
|
|
|
539
556
|
return quickLauncherConfig();
|
|
540
557
|
}
|
|
541
558
|
|
|
559
|
+
// --- Vault backend: reserved namespace, plain (unencrypted) preference ---
|
|
560
|
+
// No secret lives here either — just which of three named destinations a
|
|
561
|
+
// publish goes to — so like the quick launcher there is nothing to encrypt.
|
|
562
|
+
|
|
563
|
+
function vaultBackendConfig() {
|
|
564
|
+
const cfg = load()[VAULT_BACKEND_NAMESPACE] || {};
|
|
565
|
+
const backend = VAULT_BACKEND_VALUES.includes(cfg.backend) ? cfg.backend : DEFAULT_VAULT_BACKEND;
|
|
566
|
+
return { backend };
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function setVaultBackendConfig(backend) {
|
|
570
|
+
if (!VAULT_BACKEND_VALUES.includes(backend)) return vaultBackendConfig();
|
|
571
|
+
const data = load();
|
|
572
|
+
data[VAULT_BACKEND_NAMESPACE] = { backend };
|
|
573
|
+
save(data);
|
|
574
|
+
return vaultBackendConfig();
|
|
575
|
+
}
|
|
576
|
+
|
|
542
577
|
// --- Confirm mode: SHARED preference (AEGISONE ON8, decision D8 rule 1) --
|
|
543
578
|
// "Confirm before running tools" — the switch for the renderer's tool-call
|
|
544
579
|
// approval gate (desktop/lib/local/engine.js gatedExecuteTool). ON (default,
|
|
@@ -726,10 +761,10 @@ function createSettingsStore({ dir, safeStorage, prefsDir, env } = {}) {
|
|
|
726
761
|
function getAutoDrain() {
|
|
727
762
|
const cfg = load()[AUTO_DRAIN_NAMESPACE] || {};
|
|
728
763
|
return {
|
|
729
|
-
// Unset ===
|
|
730
|
-
//
|
|
731
|
-
//
|
|
732
|
-
enabled: cfg.enabled
|
|
764
|
+
// Unset === TRUE. The queue drains in the background unless someone
|
|
765
|
+
// switched it off in as many words; `lib/auto.js` re-reads this on every
|
|
766
|
+
// tick, so that off switch stops the very next one.
|
|
767
|
+
enabled: cfg.enabled !== false,
|
|
733
768
|
intervalMs: clampAutoDrainInterval(cfg.intervalMs),
|
|
734
769
|
};
|
|
735
770
|
}
|
|
@@ -760,7 +795,8 @@ function createSettingsStore({ dir, safeStorage, prefsDir, env } = {}) {
|
|
|
760
795
|
function getAutoMint() {
|
|
761
796
|
const cfg = load()[AUTO_MINT_NAMESPACE] || {};
|
|
762
797
|
return {
|
|
763
|
-
|
|
798
|
+
// Unset === TRUE (background sealing). `publish` below stays opt-in.
|
|
799
|
+
enabled: cfg.enabled !== false,
|
|
764
800
|
// Independent of `enabled` on purpose: an automatic seal must not drag
|
|
765
801
|
// an automatic upload along with it.
|
|
766
802
|
publish: cfg.publish === true,
|
|
@@ -807,6 +843,8 @@ function createSettingsStore({ dir, safeStorage, prefsDir, env } = {}) {
|
|
|
807
843
|
migrateLegacyAegisKey,
|
|
808
844
|
quickLauncherConfig,
|
|
809
845
|
setQuickLauncherConfig,
|
|
846
|
+
vaultBackendConfig,
|
|
847
|
+
setVaultBackendConfig,
|
|
810
848
|
getConfirmMode,
|
|
811
849
|
setConfirmMode,
|
|
812
850
|
confirmModeState,
|
|
@@ -833,6 +871,7 @@ module.exports = {
|
|
|
833
871
|
AEGIS_KEY_NAMESPACE,
|
|
834
872
|
LEGACY_AEGIS_NAMESPACE,
|
|
835
873
|
QUICK_LAUNCHER_NAMESPACE,
|
|
874
|
+
VAULT_BACKEND_NAMESPACE,
|
|
836
875
|
CONFIRM_MODE_NAMESPACE,
|
|
837
876
|
MEMORY_PERSIST_NAMESPACE,
|
|
838
877
|
AVATAR_NAMESPACE,
|
|
@@ -844,6 +883,8 @@ module.exports = {
|
|
|
844
883
|
AUTO_DRAIN_INTERVAL_MAX_MS,
|
|
845
884
|
AVATAR_MOTION_MODES,
|
|
846
885
|
DEFAULT_QUICK_LAUNCHER_SHORTCUT,
|
|
886
|
+
DEFAULT_VAULT_BACKEND,
|
|
887
|
+
VAULT_BACKEND_VALUES,
|
|
847
888
|
RESERVED_NAMESPACES,
|
|
848
889
|
isReservedNamespace,
|
|
849
890
|
maskKey,
|