aegiscode 6.3.1 → 6.3.2

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aegiscode",
3
3
  "productName": "AEGIS Code",
4
- "version": "6.3.1",
4
+ "version": "6.3.2",
5
5
  "description": "aegiscode — the command-line version of AEGIS Desktop. The shared tool surface in your shell, over the same thin transport and tool registry as the MCP plugin and the desktop app. Ships transport + UI only; no brain.",
6
6
  "author": {
7
7
  "name": "AEGIS Code",
package/src/app.js CHANGED
@@ -87,7 +87,11 @@ function createApp(options = {}) {
87
87
  const commandCtx = {
88
88
  light: opts.light,
89
89
  model: opts.model,
90
- effort: 'high',
90
+ // `null` = "auto": send no effort, so the server sizes the turn from the
91
+ // ask. This is the *budget* control on the pooled class (aegis1 sizes the
92
+ // token ladder from it), which is why it is not defaulted to a rung here —
93
+ // pinning one would make every turn cost what that rung grants.
94
+ effort: null,
91
95
  thinking: false,
92
96
  themeIndex: opts.light ? 2 : 1,
93
97
  vim: false,
@@ -371,6 +375,14 @@ function createApp(options = {}) {
371
375
  model: commandCtx.model || undefined,
372
376
  system: opts.system,
373
377
  maxTokens: opts.maxTokens,
378
+ // `effort` is the pooled budget control, and it travels on every turn
379
+ // — this host only ever runs the 'aegis' class, whose calls are sized
380
+ // server-side from it (aegis1 services/pool_brain.py). It used to be
381
+ // held in the session state and rendered in the status line without
382
+ // ever reaching the wire, so /effort changed the display and nothing
383
+ // about what the turn cost. `null` (auto) is omitted so the server
384
+ // infers the rung from the ask.
385
+ effort: commandCtx.effort || undefined,
374
386
  // false asks the engine for the buffered (non-stream) wire form, so
375
387
  // `--no-stream` and piped runs get a single body rather than SSE.
376
388
  stream: commandCtx.stream !== false,
@@ -1073,6 +1085,8 @@ function createApp(options = {}) {
1073
1085
  return;
1074
1086
  }
1075
1087
  if (!opts.model && cfg.model) commandCtx.model = cfg.model;
1088
+ // `null` in the config is "auto" and is deliberately not assigned over the
1089
+ // default — there is nothing to restore, because nothing is pinned.
1076
1090
  if (cfg.effort) commandCtx.effort = cfg.effort;
1077
1091
  if (typeof cfg.vim === 'boolean') commandCtx.vim = cfg.vim;
1078
1092
  if (cfg.lastRecap) commandCtx.lastRecap = cfg.lastRecap;
package/src/chatflow.js CHANGED
@@ -164,7 +164,7 @@ const separatorLine = (cols, ctx) => [span(themeOf(ctx).dim, '─'.repeat(cols))
164
164
  /** The idle bottom-left line: which effort level the next turn runs at. */
165
165
  function effortLine(ctx, cols) {
166
166
  const t = themeOf(ctx);
167
- const txt = `● ${ctx.effort || 'high'} · /effort`;
167
+ const txt = `● ${ctx.effort || 'auto'} · /effort`;
168
168
  const pad = ' '.repeat(Math.max(0, cols - [...txt].length - 4));
169
169
  return [span(t.gray, pad + txt)];
170
170
  }
@@ -1111,12 +1111,15 @@ async function runSession(host) {
1111
1111
  return;
1112
1112
  }
1113
1113
  if (type === 'effort') {
1114
- const levels = ['low', 'medium', 'high'];
1115
- const chosen = levels[overlay.sel || 0];
1114
+ // The picker's own order, not a second copy of the level names: this
1115
+ // used to re-declare ['low','medium','high'] here while the picker drew
1116
+ // its own table, so the row a user picked and the value it stored were
1117
+ // resolved through different lists.
1118
+ const chosen = overlays.EFFORT_VALUES[overlay.sel || 0];
1116
1119
  overlay = null;
1117
1120
  ctx.effort = chosen;
1118
1121
  host.updateConfig({ effort: chosen });
1119
- note(`effort: ${chosen}`);
1122
+ note(`effort: ${chosen || 'auto'}`);
1120
1123
  render();
1121
1124
  return;
1122
1125
  }
package/src/commands.js CHANGED
@@ -46,6 +46,7 @@ const { execSync } = require('node:child_process');
46
46
  const { span } = require('./screen.js');
47
47
  const { C, BOLD, BOLD_OFF } = require('./theme.js');
48
48
  const panels = require('./panels.js');
49
+ const overlays = require('./overlays.js');
49
50
  const {
50
51
  updateConfig, loadConfig, loadPermissions, savePermissions, addPermissionRule,
51
52
  DEFAULT_PERMISSIONS, permissionsPath, configPath,
@@ -93,7 +94,13 @@ const CATEGORIES = [
93
94
 
94
95
  const categoryLabel = (id) => (CATEGORIES.find((cat) => cat.id === id) || {}).label || id;
95
96
 
96
- const EFFORT_LEVELS = ['low', 'medium', 'high'];
97
+ // One table, shared with the picker that draws it. The two used to be separate
98
+ // lists — this one a string array, chatflow.js's own copy of the same three
99
+ // strings, and overlays.js's label/note rows — so adding a level meant editing
100
+ // three places, and the index a picker row reported was mapped back to a value
101
+ // through whichever list happened to be in scope. EFFORT_VALUES is the picker's
102
+ // own order, `null` first for "auto".
103
+ const EFFORT_LEVELS = overlays.EFFORT_VALUES;
97
104
 
98
105
  // ── Small handler helpers ─────────────────────────────────────────────────────
99
106
 
@@ -348,12 +355,12 @@ const COMMANDS = [
348
355
  },
349
356
  },
350
357
  {
351
- name: 'effort', args: ['level'], hint: '[low|medium|high]', category: 'model',
352
- desc: 'Set effort level for model usage',
358
+ name: 'effort', args: ['level'], hint: '[auto|low|medium|high]', category: 'model',
359
+ desc: 'Set the token budget the pool sizes each turn from',
353
360
  handler: async (c, args) => {
354
361
  const level = (args.level || '').toLowerCase();
355
- if (level && !EFFORT_LEVELS.includes(level)) {
356
- note(c, `Unknown effort level "${args.level}". Use ${EFFORT_LEVELS.join(', ')}.`);
362
+ if (level && level !== 'auto' && !EFFORT_LEVELS.includes(level)) {
363
+ note(c, `Unknown effort level "${args.level}". Use auto, ${EFFORT_LEVELS.filter(Boolean).join(', ')}.`);
357
364
  c.render();
358
365
  return true;
359
366
  }
@@ -361,9 +368,14 @@ const COMMANDS = [
361
368
  c.openOverlay({ type: 'effort', sel: Math.max(0, EFFORT_LEVELS.indexOf(c.ctx.effort)) });
362
369
  return true;
363
370
  }
364
- c.ctx.effort = level;
365
- c.saveConfig({ effort: level });
366
- note(c, `Effort level: ${level}`);
371
+ // 'auto' is the absence of a pin, not a fourth rung: it is stored as null
372
+ // so nothing is sent and the server sizes the turn from the ask.
373
+ const pinned = level === 'auto' ? null : level;
374
+ c.ctx.effort = pinned;
375
+ c.saveConfig({ effort: pinned });
376
+ note(c, pinned
377
+ ? `Effort level: ${pinned} — the pool sizes this turn's token budget from it`
378
+ : 'Effort level: auto — the pool sizes each turn from the ask');
367
379
  c.render();
368
380
  return true;
369
381
  },
package/src/config.js CHANGED
@@ -59,7 +59,12 @@ const DEFAULT_CONFIG = {
59
59
  // aegiscode- name so /model add/remove/switch stay compatible both ways.
60
60
  models: null,
61
61
  currentModelId: null,
62
- effort: 'high',
62
+ // `null` = no effort pinned, i.e. "auto": each turn is sized by the server
63
+ // from the ask itself (aegis1 services/pool_brain.py estimate_effort). The
64
+ // old 'high' was a *pin* on the top rung of the budget ladder — the most the
65
+ // server can grant — so the CLI's default turn was the most expensive one it
66
+ // could make, and /effort could only ever move it down.
67
+ effort: null,
63
68
  vim: false,
64
69
  lastCwd: '',
65
70
  };
package/src/overlays.js CHANGED
@@ -177,27 +177,38 @@ function renderModelPicker(models, sel = 0, width = 80, height = 24, current = n
177
177
  // ── Effort picker overlay (/effort) ──────────────────────────────────────────
178
178
 
179
179
  const EFFORT_LEVELS = [
180
- { label: 'Low', note: 'Fastest, most efficient' },
180
+ { label: 'Auto', value: null, note: 'Sized per turn from the ask (default)' },
181
+ { label: 'Low', note: 'Fastest, cheapest budget' },
181
182
  { label: 'Medium', note: 'Balanced' },
182
- { label: 'High', note: 'Highest quality, slowest' },
183
+ { label: 'High', note: 'Highest budget, slowest' },
183
184
  ];
184
185
 
186
+ // The picker's order reduced to the values a selection means, so the row a user
187
+ // picks and the value the session stores can never come from two different
188
+ // lists (commands.js validates against this, chatflow.js resolves the overlay
189
+ // through it). `null` is "auto" — no rung pinned.
190
+ const EFFORT_VALUES = EFFORT_LEVELS.map((lv) => (lv.value === undefined ? lv.label.toLowerCase() : lv.value));
191
+
185
192
  /**
186
193
  * @param {number} sel selected index
187
194
  * @param {number} width
188
- * @param {string|null} current the currently-selected effort level
195
+ * @param {string|null} current the currently-selected effort level (null = auto)
189
196
  * @param {Array} [levels] override the level table
190
197
  */
191
198
  function renderEffortPicker(sel = 0, width = 80, current = null, levels = EFFORT_LEVELS) {
192
199
  const lines = [];
193
200
  lines.push(blank(width));
194
201
  lines.push([span('', ' '), span(C.white + BOLD, 'Select effort'), span(BOLD_OFF, '')]);
195
- lines.push([span('', ' '), span(C.gray, 'Controls how much reasoning the model puts into each turn.')]);
202
+ lines.push([span('', ' '), span(C.gray, 'Sets the token budget the pool sizes each turn from.')]);
196
203
  lines.push(blank(width));
197
204
  for (let i = 0; i < levels.length; i++) {
198
205
  const lv = levels[i];
199
206
  const active = i === sel;
200
- const cur = current != null && String(current).toLowerCase() === String(lv.label).toLowerCase();
207
+ // `current == null` is auto, which is the first row's own value — matching
208
+ // on the label alone would never mark the default as the current choice.
209
+ const cur = current == null
210
+ ? lv.value === null
211
+ : String(current).toLowerCase() === String(lv.label).toLowerCase();
201
212
  const left = active ? span(C.lavender, GLYPH.cursor) : span('', ' ');
202
213
  const nameSpan = active ? span(C.lavender, lv.label) : span(C.white, lv.label);
203
214
  const mark = cur ? span(C.green, ' ' + GLYPH.check) : span('', '');
@@ -283,4 +294,6 @@ module.exports = {
283
294
  renderModelPicker,
284
295
  renderEffortPicker,
285
296
  renderResumeList,
297
+ EFFORT_LEVELS,
298
+ EFFORT_VALUES,
286
299
  };
package/src/panels.js CHANGED
@@ -202,7 +202,9 @@ function buildStatus(state, ctx = {}) {
202
202
  add('Working directory', s.cwd);
203
203
  add('Home', s.home);
204
204
  add('Model', s.model);
205
- add('Effort', s.effort);
205
+ // `null` = auto: the pool sizes each turn from the ask, so the row says so
206
+ // rather than vanishing (add() skips empty values).
207
+ add('Effort', s.effort || 'auto');
206
208
  if (s.thinking != null) add('Thinking', s.thinking ? 'on' : 'off');
207
209
  if (s.stream != null) add('Streaming', s.stream === false ? 'off' : 'on');
208
210
  if (s.vim != null) add('Vim keymap', s.vim ? 'on' : 'off');
@@ -278,6 +278,13 @@ function createClient(opts = {}) {
278
278
  * so a reasoning-only stream still counts as unanswered.
279
279
  * - `idleTimeoutMs` override the stalled-stream watchdog for this call
280
280
  * (a worker fan-out is legitimately silent between passes)
281
+ * - `maxTokens` output ceiling. Omitted from the body when absent so the
282
+ * server's own budget applies — on a pooled request that is
283
+ * the `effort` ladder, and a number sent from here caps it.
284
+ * - `effort` / `workers` pooled-brain budget rung and fan-out size.
285
+ * Forwarded only inside `extra`, as the server reads them off
286
+ * the body (aegis1 services/pool_brain.py parse_brain_request)
287
+ * and sizes the budget from them.
281
288
  */
282
289
  async function chatCompletion({
283
290
  prompt,
@@ -298,9 +305,17 @@ function createClient(opts = {}) {
298
305
  // model the server picks its default (no client-invented tier id). `mode`
299
306
  // is a legacy server-side shorthand — forwarded verbatim only when the
300
307
  // caller supplies it, never defaulted, never built into a model id.
308
+ //
309
+ // `max_tokens` is omitted entirely when the caller states none, rather than
310
+ // defaulted here. The server has its own budget ladder (`mode`/`effort` on
311
+ // the pooled path) and treats a body max_tokens as a *ceiling* over it, so
312
+ // an invented 4096 was not a harmless default: it capped every pass of a
313
+ // pooled-brain fan-out at 4096 and overrode the ladder the caller's effort
314
+ // selected. Omitting it is the documented way to say "server default" (see
315
+ // mcp/tools.js's max_tokens description).
301
316
  const body = {
302
317
  messages: buildMessages(messages, system, prompt),
303
- max_tokens: maxTokens || 4096,
318
+ ...(maxTokens ? { max_tokens: maxTokens } : {}),
304
319
  ...(extra || {}),
305
320
  };
306
321
  if (model) body.model = model;
@@ -584,13 +584,18 @@ function createLocalEngine({ aegis, settings, ollama, providers, tools, promptBu
584
584
  // model as the fan-out, one sample instead of four — otherwise
585
585
  // dropping the fan-out would have quietly changed the model too.
586
586
  ...(brainFlag === false ? { mode: 'brain' } : {}),
587
- // Only meaningful (and only sent) alongside a running fan-out — aegis1
588
- // services/pool_brain.py parse_brain_request reads `effort`/
589
- // `workers` straight off the body and clamps them itself
590
- // (EFFORT_LEVELS / MAX_WORKERS), so no client-side validation here.
591
- // Keyed on the effective brain flag, not on `autonomous`: an opted-out
592
- // single pass carries no fan-out tuning it cannot use.
593
- ...(brainFlag === true && opts.effort ? { effort: opts.effort } : {}),
587
+ // `effort` is the budget rung, and it is sent whenever the caller has
588
+ // one — not only alongside a fan-out. The fan-out is triggered by the
589
+ // model id (this class sends ``nexus-brain``), so a caller that never
590
+ // ticked "work autonomously" still ran a pooled call and had no way to
591
+ // say how big it should be: the server fell through to its own default
592
+ // rung. That is how a one-word prompt came to reserve the top of the
593
+ // ladder. A single-pass pass carries it too inert on that path, but
594
+ // it keeps the request honest about what it asked for.
595
+ ...(opts.effort ? { effort: opts.effort } : {}),
596
+ // Only meaningful with a running fan-out — aegis1 services/pool_brain.py
597
+ // parse_brain_request reads `workers` straight off the body and clamps
598
+ // it itself (MAX_WORKERS), so no client-side validation here.
594
599
  ...(brainFlag === true && opts.workers ? { workers: opts.workers } : {}),
595
600
  // The pool forwards `tools` to the provider and returns tool_calls
596
601
  // (aegis1 app.py:7765 → provider, pool_brain synthesis keeps them).