agent.libx.js 0.87.3 → 0.89.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2482,6 +2482,30 @@ function checkSyntax(path, content) {
2482
2482
  }
2483
2483
  }
2484
2484
 
2485
+ // src/reasoning.ts
2486
+ var BUDGET = { low: 2048, medium: 8192, high: 24576 };
2487
+ function toLabel(effort) {
2488
+ if (typeof effort !== "number") return effort === "off" ? "low" : effort;
2489
+ return effort <= BUDGET.low ? "low" : effort < BUDGET.high ? "medium" : "high";
2490
+ }
2491
+ function toBudget(effort) {
2492
+ return typeof effort === "number" ? effort : BUDGET[effort];
2493
+ }
2494
+ function reasoningToChatFragment(model, effort) {
2495
+ if (effort == null || effort === "off") return {};
2496
+ const provider = model.split("/")[0];
2497
+ switch (provider) {
2498
+ case "anthropic": {
2499
+ const budget = toBudget(effort);
2500
+ return { providerOptions: { thinking: { type: "enabled", budget_tokens: budget } }, maxTokens: budget + 8192 };
2501
+ }
2502
+ case "openai":
2503
+ return { providerOptions: { reasoning_effort: toLabel(effort) } };
2504
+ default:
2505
+ return {};
2506
+ }
2507
+ }
2508
+
2485
2509
  // src/Agent.ts
2486
2510
  var log3 = forComponent("Agent");
2487
2511
  var AgentOptions = class {
@@ -2559,6 +2583,10 @@ var AgentOptions = class {
2559
2583
  autoTest;
2560
2584
  /** Provider-specific options forwarded to ai.chat() (e.g. cursor mcpServers, cwd). */
2561
2585
  providerOptions;
2586
+ /** Extended-thinking / reasoning effort, normalized across providers (anthropic, openai).
2587
+ * `'off'`/undefined = none; `'low'|'medium'|'high'` or a raw token budget. Mapped to the
2588
+ * provider-specific request shape via {@link reasoningToChatFragment}; explicit `providerOptions` wins. */
2589
+ reasoning;
2562
2590
  };
2563
2591
  var Agent = class _Agent {
2564
2592
  options;
@@ -2574,6 +2602,12 @@ var Agent = class _Agent {
2574
2602
  // the assembled system prompt from the last prepare()
2575
2603
  started = false;
2576
2604
  // session-start lifecycle hook fires once per conversation
2605
+ /** Force the next `send()`/`run()` to rebuild the system prompt, tools, plan-mode and permission hooks
2606
+ * from `options` — apply mid-conversation changes to `planMode`/`permissions`/`model` etc. (prepare()
2607
+ * is otherwise memoized per conversation). */
2608
+ reprepare() {
2609
+ this.prepared = false;
2610
+ }
2577
2611
  /** Inject tools into a running agent (e.g. dynamically mounted MCP servers). Takes effect on the next turn. */
2578
2612
  addTools(tools) {
2579
2613
  this.activeTools.push(...tools);
@@ -2755,12 +2789,17 @@ var Agent = class _Agent {
2755
2789
  steps++;
2756
2790
  let res;
2757
2791
  const sent = this.trimContext();
2792
+ const frag = reasoningToChatFragment(o.model, o.reasoning);
2793
+ const reasonOpts = {
2794
+ ...frag,
2795
+ ...o.providerOptions ? { providerOptions: { ...frag.providerOptions, ...o.providerOptions } } : {}
2796
+ };
2758
2797
  try {
2759
2798
  if (useStream) {
2760
- const r = await o.ai.chat({ model: o.model, messages: sent, tools: wireTools, stream: true, signal: o.signal, providerOptions: o.providerOptions });
2799
+ const r = await o.ai.chat({ model: o.model, messages: sent, tools: wireTools, stream: true, signal: o.signal, ...reasonOpts });
2761
2800
  res = await this.consumeStream(r);
2762
2801
  } else {
2763
- const r = await o.ai.chat({ model: o.model, messages: sent, tools: wireTools, stream: false, signal: o.signal, providerOptions: o.providerOptions });
2802
+ const r = await o.ai.chat({ model: o.model, messages: sent, tools: wireTools, stream: false, signal: o.signal, ...reasonOpts });
2764
2803
  res = r;
2765
2804
  }
2766
2805
  } catch (err2) {
@@ -3881,6 +3920,7 @@ The filesystem root '/' is the real machine root \u2014 you have full filesystem
3881
3920
  return [...filtered, ...realShell, ...o.extraTools ?? []];
3882
3921
  })(),
3883
3922
  maxSteps: o.maxSteps ?? 30,
3923
+ ...o.reasoning != null ? { reasoning: o.reasoning } : {},
3884
3924
  stream: o.stream ?? false,
3885
3925
  host: o.host,
3886
3926
  hooks,
@@ -3947,6 +3987,10 @@ function loadSettings(dir) {
3947
3987
  if (raw.mcpServers && typeof raw.mcpServers === "object") cfg.mcpServers = raw.mcpServers;
3948
3988
  if (raw.permissions && typeof raw.permissions === "object") cfg.permissions = raw.permissions;
3949
3989
  if (raw.model && typeof raw.model === "string") cfg.model = raw.model;
3990
+ if (raw.reasoning != null) cfg.reasoning = raw.reasoning;
3991
+ if (raw.permissionMode === "default" || raw.permissionMode === "acceptEdits" || raw.permissionMode === "plan") cfg.permissionMode = raw.permissionMode;
3992
+ if (raw.editorMode === "normal" || raw.editorMode === "vim") cfg.editorMode = raw.editorMode;
3993
+ if (typeof raw.stream === "boolean") cfg.stream = raw.stream;
3950
3994
  if (raw.env && typeof raw.env === "object") {
3951
3995
  for (const [k, v] of Object.entries(raw.env)) {
3952
3996
  if (typeof v === "string" && !process.env[k]) process.env[k] = v;
@@ -4597,12 +4641,13 @@ function completePath(listDir, ref) {
4597
4641
  import { emitKeypressEvents } from "readline";
4598
4642
  var visibleWidth = (s) => s.replace(/\x1b\[[0-9;]*m/g, "").length;
4599
4643
  var EditorState = class _EditorState {
4600
- // placeholder full pasted content
4601
- constructor(suggest, history = [], classifyPaste, onEmptyPaste) {
4644
+ // pending operator awaiting a motion (dd/dw/cc)
4645
+ constructor(suggest, history = [], classifyPaste, onEmptyPaste, vimEnabled = false) {
4602
4646
  this.suggest = suggest;
4603
4647
  this.history = history;
4604
4648
  this.classifyPaste = classifyPaste;
4605
4649
  this.onEmptyPaste = onEmptyPaste;
4650
+ if (vimEnabled) this.vim = "insert";
4606
4651
  }
4607
4652
  suggest;
4608
4653
  history;
@@ -4629,6 +4674,24 @@ var EditorState = class _EditorState {
4629
4674
  pasteBuf = "";
4630
4675
  pasteSeq = 0;
4631
4676
  pastes = /* @__PURE__ */ new Map();
4677
+ // placeholder → full pasted content
4678
+ // Vim modal editing (opt-in): undefined = emacs/readline keymap; else 'insert'|'normal'.
4679
+ vim;
4680
+ vimOp;
4681
+ /** The most-recent history entry that EXTENDS the current buffer — the inline ghost-suggestion suffix.
4682
+ * Empty unless the buffer is a single non-empty line with the cursor at its end and no menu/search open. */
4683
+ ghost() {
4684
+ if (!this.buf || this.cursor !== this.buf.length || this.menuOpen || this.searching || this.buf.includes("\n")) return "";
4685
+ for (const h of this.history) if (h.length > this.buf.length && h.startsWith(this.buf)) return h.slice(this.buf.length);
4686
+ return "";
4687
+ }
4688
+ /** Accept the ghost suggestion (append its remainder). Returns false when there's nothing to accept. */
4689
+ acceptGhost() {
4690
+ const g = this.ghost();
4691
+ if (!g) return false;
4692
+ this.insert(g);
4693
+ return true;
4694
+ }
4632
4695
  /** Collapse pastes that would break the single-line editor (multi-line) or flood it (≥ this many chars). */
4633
4696
  static PASTE_INLINE_MAX = 800;
4634
4697
  beginPaste() {
@@ -4882,11 +4945,119 @@ var EditorState = class _EditorState {
4882
4945
  this.searchQuery = "";
4883
4946
  this.pastes.clear();
4884
4947
  this.closeMenu();
4948
+ this.vimOp = void 0;
4949
+ if (this.vim) this.vim = "insert";
4885
4950
  }
4886
4951
  };
4887
4952
  var REWIND = "\0REWIND";
4953
+ function applyVimNormal(s, key, str) {
4954
+ const k = key?.name;
4955
+ if (key?.ctrl || key?.meta) return "pass";
4956
+ if (k === "return" || k === "enter") return "pass";
4957
+ if (k === "escape") {
4958
+ s.vimOp = void 0;
4959
+ return "pass";
4960
+ }
4961
+ const op = s.vimOp;
4962
+ s.vimOp = void 0;
4963
+ if (op) {
4964
+ if (str === "d" && op === "d") s.killToStart(), s.killToEnd();
4965
+ else if (str === "c" && op === "c") {
4966
+ s.killToStart();
4967
+ s.killToEnd();
4968
+ s.vim = "insert";
4969
+ } else if (str === "w") {
4970
+ s.killWordForward();
4971
+ while (s.buf[s.cursor] === " ") s.del();
4972
+ if (op === "c") s.vim = "insert";
4973
+ }
4974
+ return "none";
4975
+ }
4976
+ switch (str) {
4977
+ case "i":
4978
+ s.vim = "insert";
4979
+ return "none";
4980
+ case "a":
4981
+ s.right();
4982
+ s.vim = "insert";
4983
+ return "none";
4984
+ case "A":
4985
+ s.end();
4986
+ s.vim = "insert";
4987
+ return "none";
4988
+ case "I":
4989
+ s.home();
4990
+ s.vim = "insert";
4991
+ return "none";
4992
+ case "o":
4993
+ s.end();
4994
+ s.insert("\n");
4995
+ s.vim = "insert";
4996
+ return "none";
4997
+ case "h":
4998
+ s.left();
4999
+ return "none";
5000
+ case "l":
5001
+ s.right();
5002
+ return "none";
5003
+ case "0":
5004
+ s.home();
5005
+ return "none";
5006
+ case "$":
5007
+ s.end();
5008
+ return "none";
5009
+ case "w":
5010
+ case "e":
5011
+ s.wordRight();
5012
+ return "none";
5013
+ case "b":
5014
+ s.wordLeft();
5015
+ return "none";
5016
+ case "x":
5017
+ s.del();
5018
+ return "none";
5019
+ case "D":
5020
+ s.killToEnd();
5021
+ return "none";
5022
+ case "C":
5023
+ s.killToEnd();
5024
+ s.vim = "insert";
5025
+ return "none";
5026
+ case "d":
5027
+ s.vimOp = "d";
5028
+ return "none";
5029
+ case "c":
5030
+ s.vimOp = "c";
5031
+ return "none";
5032
+ }
5033
+ if (k === "left") {
5034
+ s.left();
5035
+ return "none";
5036
+ }
5037
+ if (k === "right") {
5038
+ s.right();
5039
+ return "none";
5040
+ }
5041
+ if (k === "up") {
5042
+ s.historyPrev();
5043
+ return "none";
5044
+ }
5045
+ if (k === "down") {
5046
+ s.historyNext();
5047
+ return "none";
5048
+ }
5049
+ if (k === "backspace") {
5050
+ s.left();
5051
+ return "none";
5052
+ }
5053
+ return "none";
5054
+ }
4888
5055
  function applyKey(s, key, str) {
4889
5056
  const k = key?.name;
5057
+ if (s.vim === "normal" && !s.pasting && !s.searching) {
5058
+ const r = applyVimNormal(s, key ?? {}, str);
5059
+ if (r !== "pass") return r;
5060
+ }
4890
5061
  if (k === "paste-start") {
4891
5062
  s.beginPaste();
4892
5063
  return "none";
@@ -4965,13 +5136,23 @@ function applyKey(s, key, str) {
4965
5136
  }
4966
5137
  return "submit";
4967
5138
  case "tab":
4968
- if (s.menuOpen) s.accept();
5139
+ if (s.menuOpen) {
5140
+ s.accept();
5141
+ return "none";
5142
+ }
5143
+ if (s.acceptGhost()) return "none";
4969
5144
  return "none";
5145
+ // menu accept, else accept history ghost
4970
5146
  case "escape":
4971
5147
  if (s.menuOpen) {
4972
5148
  s.closeMenu();
4973
5149
  return "none";
4974
5150
  }
5151
+ if (s.vim === "insert") {
5152
+ s.vim = "normal";
5153
+ return "none";
5154
+ }
5155
+ if (s.vim === "normal" && s.buf.length) return "none";
4975
5156
  if (s.buf.length) return "cancel";
4976
5157
  if (wasEsc) return "rewind";
4977
5158
  s.prevEsc = true;
@@ -4987,8 +5168,10 @@ function applyKey(s, key, str) {
4987
5168
  s.left();
4988
5169
  return "none";
4989
5170
  case "right":
5171
+ if (s.cursor === s.buf.length && s.acceptGhost()) return "none";
4990
5172
  s.right();
4991
5173
  return "none";
5174
+ // at EOL → accept ghost
4992
5175
  case "home":
4993
5176
  s.home();
4994
5177
  return "none";
@@ -5022,13 +5205,16 @@ function createLineEditor(out) {
5022
5205
  function render(s, promptArg, maxVisible, status) {
5023
5206
  const cols = out.columns ?? 80;
5024
5207
  const mode = !s.searching ? inputMode(s.buf) : void 0;
5025
- const prompt = s.searching ? dim2(`(${s.searchMiss ? "failing " : ""}reverse-i-search)\`${s.searchQuery}': `) : mode ? COLOR[mode.name](`${mode.pfx} ${mode.name} \u203A `) : promptArg;
5208
+ const vimTag = s.vim === "normal" && !s.searching && !mode ? inverse(" N ") + " " : "";
5209
+ const prompt = s.searching ? dim2(`(${s.searchMiss ? "failing " : ""}reverse-i-search)\`${s.searchQuery}': `) : mode ? COLOR[mode.name](`${mode.pfx} ${mode.name} \u203A `) : vimTag + promptArg;
5026
5210
  if (curRow > 0) out.write(`\x1B[${curRow}A`);
5027
5211
  out.write("\r\x1B[J");
5028
5212
  const viewBuf = mode ? s.buf.slice(mode.pfx.length) : s.buf;
5029
5213
  const viewCursor = mode ? Math.max(0, s.cursor - mode.pfx.length) : s.cursor;
5030
5214
  const { rows, cursorRow, cursorCol } = wrapLayout(visibleWidth(prompt), viewBuf, viewCursor, cols);
5031
- out.write(prompt + (rows[0] ?? ""));
5215
+ const ghost = !mode && !s.searching ? s.ghost() : "";
5216
+ const ghostFits = ghost && rows.length === 1 && visibleWidth(prompt) + viewBuf.length + ghost.length < cols;
5217
+ out.write(prompt + (rows[0] ?? "") + (ghostFits ? dim2(ghost) : ""));
5032
5218
  for (let i = 1; i < rows.length; i++) out.write("\r\n" + rows[i]);
5033
5219
  const inputRows = rows.length - 1;
5034
5220
  let menuRows = 0;
@@ -5059,7 +5245,7 @@ function createLineEditor(out) {
5059
5245
  async function readLine(opts) {
5060
5246
  const maxVisible = opts.maxVisible ?? 8;
5061
5247
  if (!isTTY) return readPlainLine();
5062
- const s = new EditorState(opts.suggest, opts.history ?? [], opts.classifyPaste, opts.onEmptyPaste);
5248
+ const s = new EditorState(opts.suggest, opts.history ?? [], opts.classifyPaste, opts.onEmptyPaste, opts.vimMode);
5063
5249
  curRow = 0;
5064
5250
  if (opts.initial) s.insert(opts.initial);
5065
5251
  s.refresh();
@@ -5074,11 +5260,31 @@ function createLineEditor(out) {
5074
5260
  };
5075
5261
  process.on("SIGWINCH", onResize);
5076
5262
  return new Promise((resolve4) => {
5263
+ const redraw = () => render(s, opts.prompt, maxVisible, opts.status);
5077
5264
  const onKey = (str, key) => {
5078
5265
  if (key?.ctrl && key.name === "l") {
5079
5266
  out.write("\x1B[2J\x1B[3J\x1B[H");
5080
5267
  curRow = 0;
5081
- render(s, opts.prompt, maxVisible, opts.status);
5268
+ redraw();
5269
+ return;
5270
+ }
5271
+ if (key?.name === "tab" && key.shift && opts.onCyclePosture) {
5272
+ opts.onCyclePosture();
5273
+ redraw();
5274
+ return;
5275
+ }
5276
+ if (key?.meta && key.name === "t" && opts.onToggleThinking) {
5277
+ opts.onToggleThinking();
5278
+ redraw();
5279
+ return;
5280
+ }
5281
+ if (key?.meta && key.name === "p" && opts.onPickModel) {
5282
+ process.stdin.off("keypress", onKey);
5283
+ void opts.onPickModel().finally(() => {
5284
+ process.stdin.on("keypress", onKey);
5285
+ curRow = 0;
5286
+ redraw();
5287
+ });
5082
5288
  return;
5083
5289
  }
5084
5290
  const action = applyKey(s, key ?? {}, str);
@@ -5452,6 +5658,12 @@ function numFlag(raw, flag) {
5452
5658
  if (!Number.isFinite(n) || n < 0) throw new Error(`invalid ${flag}: ${raw ?? "(missing value)"}`);
5453
5659
  return n;
5454
5660
  }
5661
+ function parseReasoning(raw) {
5662
+ if (raw === "off" || raw === "low" || raw === "medium" || raw === "high") return raw;
5663
+ const n = Number(raw);
5664
+ if (Number.isFinite(n) && n > 0) return n;
5665
+ throw new Error(`invalid --reasoning: ${raw} (use off|low|medium|high or a token budget)`);
5666
+ }
5455
5667
  function parseArgs(argv) {
5456
5668
  const a = { stream: true, plan: false, ask: false, yes: false, vfs: false, shell: void 0, seed: false, subagents: false, help: false, version: false, cont: false, outputFormat: "text" };
5457
5669
  const rest = [];
@@ -5489,6 +5701,9 @@ function parseArgs(argv) {
5489
5701
  else if (x === "--disallowedTools" || x === "--disallowed-tools") a.disallowedTools = val(++i, x).split(",").map((s) => s.trim()).filter(Boolean);
5490
5702
  else if (x === "--append-system-prompt") a.appendSystemPrompt = val(++i, x);
5491
5703
  else if (x === "--add-dir") (a.addDirs ??= []).push(val(++i, x));
5704
+ else if (x === "--reasoning") a.reasoning = parseReasoning(val(++i, x));
5705
+ else if (x === "--session-id") a.sessionId = val(++i, x);
5706
+ else if (x === "--fork-session") a.fork = true;
5492
5707
  else if (x === "--max-steps") a.maxSteps = numFlag(argv[++i], "--max-steps");
5493
5708
  else if (x === "--max-tokens") a.maxTokens = numFlag(argv[++i], "--max-tokens");
5494
5709
  else if (x === "--timeout") a.timeoutMs = numFlag(argv[++i], "--timeout") * 1e3;
@@ -5532,6 +5747,9 @@ Flags:
5532
5747
  --append-system-prompt <t> extra instructions appended to the system prompt for this run
5533
5748
  --add-dir <path> mount another directory into the workspace (repeatable; disk mode only)
5534
5749
  --subagents allow the Task tool (spawn child agents)
5750
+ --reasoning <e> extended thinking: off|low|medium|high or a token budget (anthropic/openai)
5751
+ --session-id <id> use this id for the session (instead of an auto-generated one)
5752
+ --fork-session with -r/-c: branch the resumed session into a new id (source left untouched)
5535
5753
  --max-steps <n> step budget (default 30)
5536
5754
  --max-tokens <n> token budget kill-switch (default 200000)
5537
5755
  --timeout <sec> wall-clock kill-switch (default 120)
@@ -5543,7 +5761,7 @@ Prompts may reference files with @path (e.g. "explain @src/Agent.ts") \u2014 the
5543
5761
 
5544
5762
  Providers: set any of ANTHROPIC_API_KEY / OPENAI_API_KEY / GOOGLE_API_KEY / GROQ_API_KEY.
5545
5763
  Config: ./.agent/config.{ts,js,json} (project) or ~/.agent/config.* (user).
5546
- export default { model, maxSteps, tools, apiKeys, baseUrls, hooks, permissions, mcpServers, maxTokens,
5764
+ export default { model, maxSteps, reasoning, permissionMode, editorMode, tools, apiKeys, baseUrls, hooks, permissions, mcpServers, maxTokens,
5547
5765
  timeoutMs, maxRepeats, maxToolCalls, keepToolOutputs, maxContextTokens,
5548
5766
  learnFromMistakes, reflectOnFailure, budget: {\u2026} }
5549
5767
  hooks: { preToolUse|postToolUse|onStop: [{ tool?, command, block? }] } \u2014 shell hooks
@@ -5556,10 +5774,11 @@ Project instructions: ./AGENTS.md or ./CLAUDE.md are auto-loaded (scaffold with
5556
5774
  Auto-loaded from ./.agent/: commands/, skills/, memory/, agents/.
5557
5775
 
5558
5776
  REPL shortcuts: !<cmd> runs a shell command inline \xB7 #<note> saves a memory \xB7 @path inlines a file
5559
- REPL slash commands: /help /version /tools /permissions /status /cost /context /cwd /model /compact /rewind /undo /clear /sessions /resume /commands /skills /mcp /init /export /paste /exit
5777
+ REPL slash commands: /help /version /tools /permissions /status /cost /context /cwd /model /reasoning /config /rename /compact /rewind /undo /clear /sessions /resume /commands /skills /mcp /init /export /paste /exit
5560
5778
  REPL completion: type / (commands+skills) or @ (files) for a LIVE menu \u2014 \u2191/\u2193 select, \u23CE/Tab accept, Esc dismiss.
5561
5779
  REPL multi-line: Option/Alt+Enter inserts a newline, or end a line with \\ to continue. Esc cancels a running turn / clears the input line; double-Esc jumps back to edit a previous message.
5562
- REPL editing (emacs/readline): Ctrl-A/E line start/end \xB7 Ctrl-B/F char \xB7 Alt-B/F or Alt/Ctrl-\u2190/\u2192 word \xB7 Ctrl-W kill word \xB7 Ctrl-U/K kill to start/end \xB7 Ctrl-Y yank \xB7 Alt-D kill word fwd \xB7 Ctrl-L clear screen.
5780
+ REPL shortcuts: Shift+Tab cycles permission posture (ask \u2192 accept-edits \u2192 plan) \xB7 Alt+T toggles reasoning \xB7 Alt+P switches model \xB7 \u2192 or Tab accepts the dim history ghost-suggestion.
5781
+ REPL editing (emacs/readline): Ctrl-A/E line start/end \xB7 Ctrl-B/F char \xB7 Alt-B/F or Alt/Ctrl-\u2190/\u2192 word \xB7 Ctrl-W kill word \xB7 Ctrl-U/K kill to start/end \xB7 Ctrl-Y yank \xB7 Alt-D kill word fwd \xB7 Ctrl-L clear screen. Set editorMode:'vim' (or /config) for modal vim editing.
5563
5782
  REPL paste: large/multi-line pastes collapse to a [Pasted text +N lines] preview (expands on send); a pasted image/file path attaches as [Image]/[File]; /paste grabs a clipboard image (macOS).`;
5564
5783
  function newestModel() {
5565
5784
  return listModels().slice().sort((a, b) => (getModelInfo(b)?.releaseDate ?? "").localeCompare(getModelInfo(a)?.releaseDate ?? ""))[0] ?? "";
@@ -5875,6 +6094,7 @@ function optsFor(args, ai, cfg = {}, extraTools = []) {
5875
6094
  permissions: rules.length ? new PermissionPolicy({ rules, default: "allow", host: makeHost(), ask: makeAskResolver(resolve3(args.cwd ?? process.cwd())) }) : void 0,
5876
6095
  subagents: args.subagents || cfg.subagents || false,
5877
6096
  maxSteps: args.maxSteps ?? cfg.maxSteps,
6097
+ reasoning: args.reasoning ?? cfg.reasoning,
5878
6098
  // kill-switches: CLI flag > config > Agent default
5879
6099
  maxTokens: args.maxTokens ?? cfg.maxTokens,
5880
6100
  timeoutMs: args.timeoutMs ?? cfg.timeoutMs,
@@ -6069,6 +6289,14 @@ function startSession(args, store, agent, cwd) {
6069
6289
  const data = args.resume ? store.load(args.resume) : store.latestData();
6070
6290
  if (data) {
6071
6291
  agent.transcript = data.messages;
6292
+ if (args.fork) {
6293
+ const now2 = Date.now();
6294
+ const forked = { meta: { ...data.meta, id: args.sessionId ?? store.newId(now2), created: now2, updated: now2, turns: data.meta.turns }, messages: data.messages };
6295
+ err(dim(` forked ${data.meta.id} \u2192 ${forked.meta.id} (${data.meta.turns} turns)
6296
+ `));
6297
+ if (!args.task) printHistory(data.messages);
6298
+ return forked;
6299
+ }
6072
6300
  err(dim(` resumed session ${data.meta.id} (${data.meta.turns} turns) \u2014 ${data.meta.title}
6073
6301
  `));
6074
6302
  if (!args.task) printHistory(data.messages);
@@ -6078,7 +6306,7 @@ function startSession(args, store, agent, cwd) {
6078
6306
  `));
6079
6307
  }
6080
6308
  const now = Date.now();
6081
- return { meta: { id: store.newId(now), created: now, updated: now, cwd, model: agent.options.model, turns: 0, title: "" }, messages: [] };
6309
+ return { meta: { id: args.sessionId ?? store.newId(now), created: now, updated: now, cwd, model: agent.options.model, turns: 0, title: "" }, messages: [] };
6082
6310
  }
6083
6311
  var AGENTS_MD_TEMPLATE = `# ${"${name}"}
6084
6312
 
@@ -6109,23 +6337,56 @@ function initInstructions(cwd) {
6109
6337
  err(green(` created ${path}
6110
6338
  `) + dim(" edit it, then it auto-loads into every run.\n"));
6111
6339
  }
6112
- function persistModel(cwd, model) {
6340
+ function persistSetting(cwd, key, value) {
6113
6341
  const path = join8(cwd, ".agent", "settings.json");
6114
6342
  try {
6115
6343
  const obj = existsSync7(path) ? JSON.parse(readFileSync5(path, "utf8")) : {};
6116
- if (obj.model === model) return;
6117
- obj.model = model;
6344
+ if (obj[key] === value) return;
6345
+ obj[key] = value;
6118
6346
  mkdirSync6(dirname3(path), { recursive: true });
6119
6347
  writeFileSync6(path, JSON.stringify(obj, null, 2) + "\n");
6120
6348
  } catch (e) {
6121
- err(yellow(` \u26A0 couldn't persist model to ${path} \u2014 ${e?.message ?? e}
6349
+ err(yellow(` \u26A0 couldn't persist ${key} to ${path} \u2014 ${e?.message ?? e}
6122
6350
  `));
6123
6351
  }
6124
6352
  }
6353
+ var persistModel = (cwd, model) => persistSetting(cwd, "model", model);
6125
6354
  async function repl(args, ai, cfg, cwd) {
6126
6355
  const oauth = new McpOAuth({ storePath: join8(cwd, ".agent", "mcp-auth.json") });
6127
6356
  const mounted = await mountMcp(cfg, oauth);
6128
6357
  const agent = await makeAgent(args, ai, cfg, mounted.flatMap((m) => m.tools));
6358
+ const baseRules = () => [
6359
+ ...parsePermRules({ deny: args.disallowedTools }),
6360
+ ...parsePermRules({ allow: args.allowedTools }),
6361
+ ...parsePermRules(mergePerms(loadPersistedRules(cwd), cfg.permissions))
6362
+ ];
6363
+ const askFor = (tools) => tools.map((t) => ({ tool: t, decision: "ask" }));
6364
+ const POSTURES = ["default", "acceptEdits", "plan"];
6365
+ let posture = args.plan || cfg.permissionMode === "plan" ? "plan" : cfg.permissionMode === "acceptEdits" ? "acceptEdits" : "default";
6366
+ const postureLabel = () => posture === "default" ? "ask (default)" : posture === "acceptEdits" ? "accept edits" : "plan mode";
6367
+ const applyPosture = (p) => {
6368
+ posture = p;
6369
+ const ask = p === "acceptEdits" ? askFor(["bash", "Shell"]) : askFor(ASK_MUTATING);
6370
+ agent.options.permissions = new PermissionPolicy({ rules: [...baseRules(), ...ask], default: "allow", host: makeHost(), ask: makeAskResolver(cwd) });
6371
+ agent.options.planMode = p === "plan";
6372
+ agent.reprepare();
6373
+ };
6374
+ const cyclePosture = () => {
6375
+ applyPosture(POSTURES[(POSTURES.indexOf(posture) + 1) % POSTURES.length]);
6376
+ err(dim(` \u21E5 ${postureLabel()}
6377
+ `));
6378
+ return postureLabel();
6379
+ };
6380
+ if (!args.yes) applyPosture(posture);
6381
+ const REASONING_CYCLE = ["off", "low", "medium", "high"];
6382
+ const toggleReasoning = () => {
6383
+ const cur = String(agent.options.reasoning ?? "off");
6384
+ const next = REASONING_CYCLE[(Math.max(0, REASONING_CYCLE.indexOf(cur)) + 1) % REASONING_CYCLE.length];
6385
+ agent.options.reasoning = next;
6386
+ err(dim(` ~ reasoning \u2192 ${next}
6387
+ `));
6388
+ return next;
6389
+ };
6129
6390
  const pendingImages = [];
6130
6391
  const grabClipboardAttachment = () => {
6131
6392
  const dir = join8(tmpdir(), "agentx-pasted");
@@ -6144,6 +6405,18 @@ async function repl(args, ai, cfg, cwd) {
6144
6405
  void closeMcp(mounted);
6145
6406
  process.exit(130);
6146
6407
  });
6408
+ const isStreamTeardown = (e) => /NGHTTP2_FRAME_SIZE_ERROR|ERR_HTTP2_STREAM_ERROR/.test(`${e?.code ?? ""} ${e?.rawMessage ?? ""} ${e?.message ?? ""}`);
6409
+ const onFatal = (kind) => (e) => {
6410
+ if (isStreamTeardown(e)) {
6411
+ log11.debug(`suppressed ${kind} (connect/HTTP-2 stream teardown on cancel)`, e);
6412
+ return;
6413
+ }
6414
+ console.error(e);
6415
+ void closeMcp(mounted);
6416
+ process.exit(1);
6417
+ };
6418
+ process.on("unhandledRejection", onFatal("unhandledRejection"));
6419
+ process.on("uncaughtException", onFatal("uncaughtException"));
6147
6420
  const store = new SessionStore(cwd);
6148
6421
  let session = startSession(args, store, agent, cwd);
6149
6422
  const checkpoints = args.vfs || args.boddb ? new CheckpointStack(agent.options.fs) : new GitCheckpoints({ workTree: cwd, gitDir: join8(cwd, ".agent", "checkpoints.git"), addDirs: args.addDirs, sessionId: session.meta.id });
@@ -6401,6 +6674,92 @@ ${extra}` : body, checkpoints, cwd);
6401
6674
  } else err(dim(" " + agent.options.model + "\n"));
6402
6675
  }
6403
6676
  },
6677
+ reasoning: {
6678
+ desc: "extended thinking \u2014 /reasoning <off|low|medium|high|tokens>, or alone for an interactive picker",
6679
+ run: async (a) => {
6680
+ const current = String(agent.options.reasoning ?? "off");
6681
+ let next;
6682
+ if (a[0]) {
6683
+ try {
6684
+ next = parseReasoning(a[0]);
6685
+ } catch (e) {
6686
+ err(yellow(" " + (e?.message ?? e) + "\n"));
6687
+ return;
6688
+ }
6689
+ } else {
6690
+ const items = [
6691
+ { label: "off", value: "off", desc: "no extended thinking" },
6692
+ { label: "low", value: "low", desc: "minimal reasoning (~2k tokens)" },
6693
+ { label: "medium", value: "medium", desc: "balanced (~8k tokens)" },
6694
+ { label: "high", value: "high", desc: "maximal reasoning (~24k tokens)" }
6695
+ ];
6696
+ const picked = await selectMenu(process.stderr, { title: `Reasoning effort \xB7 current: ${current}`, items, current });
6697
+ if (!picked) {
6698
+ err(dim(" " + current + "\n"));
6699
+ return;
6700
+ }
6701
+ next = picked;
6702
+ }
6703
+ agent.options.reasoning = next;
6704
+ if (next !== "off" && getModelInfo(agent.options.model)?.reasoning === false)
6705
+ err(yellow(` note: ${agent.options.model} has no reasoning capability \u2014 setting may be ignored
6706
+ `));
6707
+ err(dim(" reasoning \u2192 " + next + "\n"));
6708
+ }
6709
+ },
6710
+ config: {
6711
+ desc: "view/change settings \u2014 model, reasoning, permission posture, streaming, editor mode",
6712
+ run: async () => {
6713
+ for (; ; ) {
6714
+ const items = [
6715
+ { label: "model", value: "model", desc: agent.options.model },
6716
+ { label: "reasoning", value: "reasoning", desc: String(agent.options.reasoning ?? "off") },
6717
+ { label: "permission posture", value: "posture", desc: postureLabel() + " (Shift+Tab)" },
6718
+ { label: "streaming", value: "stream", desc: agent.options.stream ? "on" : "off" },
6719
+ { label: "editor mode", value: "editor", desc: cfg.editorMode === "vim" ? "vim" : "normal" }
6720
+ ];
6721
+ const pick = await selectMenu(process.stderr, { title: "Settings \xB7 \u21B5 change \xB7 esc close", items });
6722
+ if (!pick) return;
6723
+ if (pick === "model") {
6724
+ const m = await pickModel(agent.options.model);
6725
+ if (m) {
6726
+ agent.options.model = m;
6727
+ persistModel(cwd, m);
6728
+ }
6729
+ } else if (pick === "reasoning") {
6730
+ await builtins.reasoning.run([]);
6731
+ persistSetting(cwd, "reasoning", agent.options.reasoning ?? "off");
6732
+ } else if (pick === "posture") {
6733
+ cyclePosture();
6734
+ persistSetting(cwd, "permissionMode", posture);
6735
+ } else if (pick === "stream") {
6736
+ agent.options.stream = !agent.options.stream;
6737
+ persistSetting(cwd, "stream", agent.options.stream);
6738
+ err(dim(" streaming \u2192 " + (agent.options.stream ? "on" : "off") + "\n"));
6739
+ } else if (pick === "editor") {
6740
+ cfg.editorMode = cfg.editorMode === "vim" ? "normal" : "vim";
6741
+ persistSetting(cwd, "editorMode", cfg.editorMode);
6742
+ err(dim(" editor \u2192 " + cfg.editorMode + "\n"));
6743
+ }
6744
+ }
6745
+ }
6746
+ },
6747
+ rename: {
6748
+ desc: "rename the current session \u2014 /rename <title>",
6749
+ run: (a) => {
6750
+ const t = a.join(" ").trim();
6751
+ if (!t) {
6752
+ err(dim(" title: " + (session.meta.title || "(none)") + "\n"));
6753
+ return;
6754
+ }
6755
+ session.meta.title = t;
6756
+ try {
6757
+ store.save(session);
6758
+ } catch {
6759
+ }
6760
+ err(dim(" renamed \u2192 " + t + "\n"));
6761
+ }
6762
+ },
6404
6763
  compact: {
6405
6764
  desc: "summarize older context to free up the window",
6406
6765
  run: () => {
@@ -6695,11 +7054,20 @@ ${extra}` : body, checkpoints, cwd);
6695
7054
  return { hits, token, describe };
6696
7055
  };
6697
7056
  const editor = createLineEditor(process.stderr);
7057
+ let aborting = false;
7058
+ let pendingRewind = false;
6698
7059
  if (process.stdin.isTTY) {
6699
7060
  process.stdin.on("keypress", (_s, key) => {
6700
- if (activeTurn && (key?.name === "escape" || key?.ctrl && key?.name === "c")) {
7061
+ if (!activeTurn) return;
7062
+ const cancel = key?.name === "escape" || key?.ctrl && key?.name === "c";
7063
+ if (!cancel) return;
7064
+ if (!aborting) {
7065
+ aborting = true;
6701
7066
  activeTurn.abort();
6702
7067
  err(yellow("\n \u238B cancelling\u2026\n"));
7068
+ } else if (key?.name === "escape" && !pendingRewind) {
7069
+ pendingRewind = true;
7070
+ err(dim(" \u238B\u238B jumping back to edit\u2026\n"));
6703
7071
  }
6704
7072
  });
6705
7073
  }
@@ -6717,14 +7085,48 @@ ${extra}` : body, checkpoints, cwd);
6717
7085
  };
6718
7086
  let prefill;
6719
7087
  while (true) {
7088
+ if (pendingRewind) {
7089
+ pendingRewind = false;
7090
+ const t = await rewindToMessage();
7091
+ if (t !== void 0) prefill = t;
7092
+ }
7093
+ aborting = false;
6720
7094
  err("\n");
6721
7095
  const initial = prefill;
6722
7096
  prefill = void 0;
6723
7097
  const ctxTok = estimateTranscriptTokens(agent.transcript);
6724
7098
  const ctxCap = agent.options.maxTokens || 2e5;
6725
7099
  const usd = session.meta.costUsd ?? 0;
6726
- const footer = ctxTok > 400 || usd > 0 ? `${Math.round(ctxTok / ctxCap * 100)}% ctx (~${(ctxTok / 1e3).toFixed(1)}k/${Math.round(ctxCap / 1e3)}k)${usd > 0 ? ` \xB7 ${session.meta.costEstimated ? "~" : ""}${fmtUsd(usd)}` : ""}` : "";
6727
- const result = await readMultiline((cont) => editor.readLine({ prompt: cont ? contPrompt : promptStr, suggest, history, classifyPaste, onEmptyPaste: grabClipboardAttachment, initial: cont ? void 0 : initial, status: () => footer }));
7100
+ const computeFooter = () => {
7101
+ const parts = [];
7102
+ if (ctxTok > 400) parts.push(`${Math.round(ctxTok / ctxCap * 100)}% ctx (~${(ctxTok / 1e3).toFixed(1)}k/${Math.round(ctxCap / 1e3)}k)`);
7103
+ if (usd > 0) parts.push(`${session.meta.costEstimated ? "~" : ""}${fmtUsd(usd)}`);
7104
+ if (posture !== "default") parts.push(postureLabel());
7105
+ const r = agent.options.reasoning;
7106
+ if (r && r !== "off") parts.push(`reasoning:${r}`);
7107
+ return parts.join(" \xB7 ");
7108
+ };
7109
+ const result = await readMultiline((cont) => editor.readLine({
7110
+ prompt: cont ? contPrompt : promptStr,
7111
+ suggest,
7112
+ history,
7113
+ classifyPaste,
7114
+ onEmptyPaste: grabClipboardAttachment,
7115
+ initial: cont ? void 0 : initial,
7116
+ status: computeFooter,
7117
+ vimMode: cfg.editorMode === "vim",
7118
+ onCyclePosture: cyclePosture,
7119
+ onToggleThinking: toggleReasoning,
7120
+ onPickModel: async () => {
7121
+ const picked = await pickModel(agent.options.model);
7122
+ if (picked) {
7123
+ agent.options.model = picked;
7124
+ persistModel(cwd, picked);
7125
+ err(dim(" model \u2192 " + picked + "\n"));
7126
+ }
7127
+ return picked;
7128
+ }
7129
+ }));
6728
7130
  if (result === null) break;
6729
7131
  if (result === REWIND) {
6730
7132
  prefill = await rewindToMessage();