sapper-iq 1.4.6 → 1.4.8

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/sapper.mjs +684 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sapper-iq",
3
- "version": "1.4.6",
3
+ "version": "1.4.8",
4
4
  "description": "AI-powered development assistant that executes commands and builds projects",
5
5
  "main": "sapper.mjs",
6
6
  "bin": {
package/sapper.mjs CHANGED
@@ -656,6 +656,14 @@ const TOOL_NAME_MAP = {
656
656
  'ask_expert': 'CONSULT',
657
657
  'expert': 'CONSULT',
658
658
  'advisor': 'CONSULT',
659
+ 'think': 'DEEPTHINK',
660
+ 'deepthink': 'DEEPTHINK',
661
+ 'deep_think': 'DEEPTHINK',
662
+ 'talk': 'DEEPTHINK',
663
+ 'talk_about': 'DEEPTHINK',
664
+ 'discuss': 'DEEPTHINK',
665
+ 'moe': 'DEEPTHINK',
666
+ 'reason': 'DEEPTHINK',
659
667
  'todo': 'LIST', // alias — list tasks
660
668
  };
661
669
 
@@ -683,6 +691,7 @@ const TOOL_ALLOWED_BY = {
683
691
  OPEN: ['OPEN', 'SHELL'],
684
692
  SHELL: ['SHELL'],
685
693
  CONSULT: ['CONSULT'],
694
+ DEEPTHINK: ['DEEPTHINK'],
686
695
  };
687
696
 
688
697
  function normalizeToolName(toolName = '') {
@@ -1171,8 +1180,31 @@ const DEFAULT_CONFIG = Object.freeze({
1171
1180
  tools: ['read', 'read_chunk', 'list', 'search', 'grep', 'find', 'regex', 'head', 'tail', 'cat', 'pwd', 'changes', 'fetch_web', 'recall_memory', 'search_memory_notes', 'read_memory_notes'], // Read-only subset by default — no write / patch / shell / open / mkdir / rmdir.
1172
1181
  saveTranscripts: true, // Persist every consultation to disk for audit.
1173
1182
  transcriptDir: '.sapper/consultations', // Where transcripts are written.
1183
+ verbose: true, // Print full request, each tool call/result, and final answer to the terminal during the consultation.
1174
1184
  systemPrompt: '', // Override the built-in consultant system prompt (empty = use default).
1175
1185
  }),
1186
+ deepthink: Object.freeze({
1187
+ // Multi-turn back-and-forth with a reasoning model — like Claude Opus thinking.
1188
+ // Unlike the consultant (one-shot), this preserves a thought-session across calls so the
1189
+ // main agent can have a real back-and-forth: ask, get a thoughtful answer, ask follow-up,
1190
+ // refine, etc. Use when the main agent hits something unexpected or needs deep reasoning.
1191
+ enabled: true,
1192
+ model: 'juilpark/Qwen3.5-27B-Claude-4.6-Opus-Reasoning-Distilled-heretic:q4_k_m', // Pick a strong reasoning model.
1193
+ contextLimit: null, // Tokens to give the thinker (null = use model default).
1194
+ temperature: 0.7, // Higher than consultant — reasoning benefits from a little exploration.
1195
+ thinking: 'on', // 'on' | 'off' | 'auto' — should usually stay on for reasoning models.
1196
+ maxTurnsPerSession: 30, // Max back-and-forth exchanges in one thought session.
1197
+ maxSessions: 5, // Max concurrent thought sessions kept in memory.
1198
+ sessionIdleTimeoutMs: 1800000, // 30 min — idle sessions are evicted.
1199
+ perTurnTimeoutMs: 180000, // 3 min hard cap on a single thought turn.
1200
+ maxMessageChars: 16000, // Reject incoming messages larger than this.
1201
+ tools: ['read', 'read_chunk', 'list', 'search', 'grep', 'find', 'regex', 'head', 'tail', 'cat', 'pwd', 'changes', 'fetch_web', 'recall_memory', 'search_memory_notes', 'read_memory_notes'], // Read-only subset — thinker can verify code itself instead of needing everything inline. Set to [] to make the thinker tool-less (pure reasoning from the message).
1202
+ toolRoundLimit: 6, // Max tool-call rounds the thinker may run within a single turn before being forced to answer.
1203
+ saveTranscripts: true, // Persist every thought session to disk for audit.
1204
+ transcriptDir: '.sapper/thinking', // Where thought transcripts are written.
1205
+ verbose: true, // Print each thought exchange to the terminal.
1206
+ systemPrompt: '', // Override the built-in deep-think system prompt (empty = use default).
1207
+ }),
1176
1208
  prompt: Object.freeze({
1177
1209
  prepend: '',
1178
1210
  append: '',
@@ -1192,7 +1224,7 @@ RULES:
1192
1224
  5. NO HALLUCINATIONS: If a file doesn't exist, don't guess its content. List the directory instead.`,
1193
1225
  nativeTools: `TOOLS:
1194
1226
  You have function-calling tools available. Call them directly — do NOT use [TOOL:...] text markers.
1195
- Available tools: list_directory, read_file, search_files, write_file, patch_file, create_directory, ls, cat, head, tail, grep, find, regex_search, read_chunk, pwd, cd, rmdir, changes, fetch_web, recall_memory, save_memory_note, search_memory_notes, read_memory_notes, open_url, run_shell, consult_expert.
1227
+ Available tools: list_directory, read_file, search_files, write_file, patch_file, create_directory, ls, cat, head, tail, grep, find, regex_search, read_chunk, pwd, cd, rmdir, changes, fetch_web, recall_memory, save_memory_note, search_memory_notes, read_memory_notes, open_url, run_shell, consult_expert, deep_think.
1196
1228
 
1197
1229
  PATCH TIPS:
1198
1230
  - For patch_file, set old_text to "LINE:<number>" to replace a specific line by number (most reliable).
@@ -1220,6 +1252,12 @@ CONSULTANT (consult_expert):
1220
1252
  - DO NOT call it casually. Every call requires: goal (what important thing you're doing), question (specific decision you need help with), summary (≥25 words of what you've explored and why you're stuck), and files (paths the consultant should read — use "path:start-end" for line ranges on large files).
1221
1253
  - The consultant is read-only. It will use its own tools to verify facts and will return a RECOMMENDATION / REASONING / RISKS / NEXT STEPS answer that you then act on.
1222
1254
 
1255
+ DEEP THINK (deep_think):
1256
+ - deep_think opens a multi-turn back-and-forth with a separate reasoning model (think Claude Opus thinking). Use when something is unexpected, the logic is twisted, or you want to think out loud with a smarter model across several turns.
1257
+ - The thinker has a small set of READ-ONLY tools (read_file, read_chunk, list_directory, search_files, grep, find, regex_search, head, tail, cat, pwd, changes, fetch_web, recall_memory, search_memory_notes, read_memory_notes — actual set is configurable). It can verify code itself, so you do NOT need to inline everything; mentioning paths and what to look at is enough. Still include relevant errors and constraints in the message.
1258
+ - Each call returns a session_id. To continue the SAME thought across multiple exchanges, pass that session_id back. To start fresh, omit session_id or set new_session: true.
1259
+ - Sessions are capped at a configured number of turns. Use them as real conversations, not one-shot queries: ask, get reply, then ask follow-up that builds on it.
1260
+
1223
1261
  SHELL TIPS:
1224
1262
  - run_shell may keep long-running commands in a background session depending on config.
1225
1263
  - If a shell result returns a session id, inspect more output with run_shell command "__shell_read__ <session_id>".
@@ -1253,6 +1291,9 @@ SHELL TIPS:
1253
1291
  - [TOOL:OPEN]https://example.com[/TOOL] - Open a URL in the default browser (asks for approval)
1254
1292
  - [TOOL:SHELL]command[/TOOL] - Run shell command
1255
1293
  - [TOOL:CONSULT]goal:::question:::summary (>=25 words):::attempts:::hints:::file1,file2:start-end[/TOOL] - Call the heavyweight consultant model for advice. ONLY when stuck or before an important change. Or pass a JSON blob: [TOOL:CONSULT]{"goal":"...","question":"...","summary":"...","files":["src/x.ts:40-120"]}[/TOOL]
1294
+ - [TOOL:THINK]<message>[/TOOL] - Open a multi-turn back-and-forth with a separate reasoning model. New session.
1295
+ - [TOOL:THINK]<session_id>:::<follow-up message>[/TOOL] - Continue a previous thought session (use the session_id from the prior reply).
1296
+ - [TOOL:THINK]{"message":"...","session_id":"think-abc-1"}[/TOOL] - JSON form for clarity.
1256
1297
 
1257
1298
  PATCH TIPS:
1258
1299
  - PREFER the LINE:number mode when you know which line to change. It is much more reliable than text matching.
@@ -1591,10 +1632,59 @@ function normalizeConsultantConfig(consultantConfig = {}) {
1591
1632
  transcriptDir: typeof consultantConfig.transcriptDir === 'string' && consultantConfig.transcriptDir.trim()
1592
1633
  ? consultantConfig.transcriptDir.trim()
1593
1634
  : D.transcriptDir,
1635
+ verbose: normalizeBoolean(consultantConfig.verbose, D.verbose),
1594
1636
  systemPrompt: typeof consultantConfig.systemPrompt === 'string' ? consultantConfig.systemPrompt : D.systemPrompt,
1595
1637
  };
1596
1638
  }
1597
1639
 
1640
+ function normalizeDeepthinkConfig(deepthinkConfig = {}) {
1641
+ if (typeof deepthinkConfig === 'boolean') {
1642
+ return { ...DEFAULT_CONFIG.deepthink, enabled: deepthinkConfig };
1643
+ }
1644
+ if (typeof deepthinkConfig === 'string') {
1645
+ return { ...DEFAULT_CONFIG.deepthink, enabled: normalizeBoolean(deepthinkConfig, DEFAULT_CONFIG.deepthink.enabled) };
1646
+ }
1647
+ if (!deepthinkConfig || typeof deepthinkConfig !== 'object' || Array.isArray(deepthinkConfig)) {
1648
+ return { ...DEFAULT_CONFIG.deepthink };
1649
+ }
1650
+ const D = DEFAULT_CONFIG.deepthink;
1651
+ return {
1652
+ enabled: normalizeBoolean(deepthinkConfig.enabled, D.enabled),
1653
+ model: typeof deepthinkConfig.model === 'string' && deepthinkConfig.model.trim()
1654
+ ? deepthinkConfig.model.trim()
1655
+ : D.model,
1656
+ contextLimit: normalizeContextLimit(deepthinkConfig.contextLimit),
1657
+ temperature: (() => {
1658
+ const v = Number(deepthinkConfig.temperature);
1659
+ return Number.isFinite(v) && v >= 0 && v <= 2 ? v : D.temperature;
1660
+ })(),
1661
+ thinking: normalizeThinkingMode(deepthinkConfig.thinking),
1662
+ maxTurnsPerSession: normalizeIntegerInRange(deepthinkConfig.maxTurnsPerSession, D.maxTurnsPerSession, 1, 500),
1663
+ maxSessions: normalizeIntegerInRange(deepthinkConfig.maxSessions, D.maxSessions, 1, 50),
1664
+ sessionIdleTimeoutMs: normalizeIntegerInRange(deepthinkConfig.sessionIdleTimeoutMs, D.sessionIdleTimeoutMs, 60000, 7200000),
1665
+ perTurnTimeoutMs: normalizeIntegerInRange(deepthinkConfig.perTurnTimeoutMs, D.perTurnTimeoutMs, 10000, 1800000),
1666
+ maxMessageChars: normalizeIntegerInRange(deepthinkConfig.maxMessageChars, D.maxMessageChars, 100, 200000),
1667
+ tools: (() => {
1668
+ const raw = deepthinkConfig.tools;
1669
+ if (raw === undefined || raw === null) return [...D.tools];
1670
+ if (Array.isArray(raw)) return raw.map(t => String(t || '').trim()).filter(Boolean);
1671
+ if (typeof raw === 'string') {
1672
+ const trimmed = raw.trim().toLowerCase();
1673
+ if (trimmed === '' || trimmed === 'none' || trimmed === 'off') return [];
1674
+ return raw.split(',').map(t => t.trim()).filter(Boolean);
1675
+ }
1676
+ return [...D.tools];
1677
+ })(),
1678
+ toolRoundLimit: normalizeIntegerInRange(deepthinkConfig.toolRoundLimit, D.toolRoundLimit, 0, 50),
1679
+ saveTranscripts: normalizeBoolean(deepthinkConfig.saveTranscripts, D.saveTranscripts),
1680
+ transcriptDir: typeof deepthinkConfig.transcriptDir === 'string' && deepthinkConfig.transcriptDir.trim()
1681
+ ? deepthinkConfig.transcriptDir.trim()
1682
+ : D.transcriptDir,
1683
+ verbose: normalizeBoolean(deepthinkConfig.verbose, D.verbose),
1684
+ systemPrompt: typeof deepthinkConfig.systemPrompt === 'string' ? deepthinkConfig.systemPrompt : D.systemPrompt,
1685
+ };
1686
+ }
1687
+
1598
1688
  function normalizePromptText(value) {
1599
1689
  if (typeof value === 'string') return value;
1600
1690
  if (value === null || value === undefined) return '';
@@ -1641,6 +1731,7 @@ function normalizeConfig(config = {}) {
1641
1731
  chunking: normalizeChunkingConfig(config.chunking),
1642
1732
  voice: normalizeVoiceConfig(config.voice),
1643
1733
  consultant: normalizeConsultantConfig(config.consultant),
1734
+ deepthink: normalizeDeepthinkConfig(config.deepthink),
1644
1735
  prompt: normalizePromptConfig(config.prompt),
1645
1736
  };
1646
1737
  }
@@ -1773,6 +1864,13 @@ function renderConfigFile(config) {
1773
1864
  lines.push(' // Tip: pick a stronger reasoning model than your main model — that is the whole point.');
1774
1865
  appendConfigProperty(lines, 'consultant', config.consultant);
1775
1866
 
1867
+ lines.push('');
1868
+ lines.push(' // Deep-think tool — multi-turn back-and-forth with a reasoning model.');
1869
+ lines.push(' // Use when the agent hits something unexpected or needs deep reasoning.');
1870
+ lines.push(' // Each call extends an ongoing thought session (pass session_id) or starts a new one.');
1871
+ lines.push(' // Sessions cap at maxTurnsPerSession exchanges; idle sessions are evicted automatically.');
1872
+ appendConfigProperty(lines, 'deepthink', config.deepthink);
1873
+
1776
1874
  lines.push('');
1777
1875
  lines.push(' // Prompt customization');
1778
1876
  lines.push(' // prompt.system.* controls the assistant system prompt blocks');
@@ -2002,6 +2100,14 @@ function consultantEnabled() {
2002
2100
  return getConsultantConfig().enabled;
2003
2101
  }
2004
2102
 
2103
+ function getDeepthinkConfig() {
2104
+ return normalizeDeepthinkConfig(sapperConfig.deepthink);
2105
+ }
2106
+
2107
+ function deepthinkEnabled() {
2108
+ return getDeepthinkConfig().enabled;
2109
+ }
2110
+
2005
2111
  function streamPhaseStatusEnabled() {
2006
2112
  return getStreamingConfig().showPhaseStatus;
2007
2113
  }
@@ -3151,6 +3257,7 @@ const COMMAND_GROUPS = Object.freeze([
3151
3257
  ['/shell read <id>', 'Read output from a tracked shell session'],
3152
3258
  ['/shell stop <id>', 'Stop a tracked shell session'],
3153
3259
  ['/consult', 'Show or configure the heavyweight consultant tool (separate model)'],
3260
+ ['/think', 'Show or configure the deep-think tool (multi-turn reasoning with a separate model)'],
3154
3261
  ['/context', 'Inspect token usage, summary trigger, and model window'],
3155
3262
  ['/ctx <limit>', 'Set context window limit (e.g. /ctx 64k)'],
3156
3263
  ['/debug', 'Toggle regex and tool debug output'],
@@ -5901,6 +6008,34 @@ async function consultExpert({ summary, question, goal, attempts, hints, files }
5901
6008
  'Consulting expert', 'magenta'
5902
6009
  ));
5903
6010
 
6011
+ const verbose = cfg.verbose !== false;
6012
+ const dim = chalk.gray;
6013
+ const accent = chalk.hex('#b7b9ff'); // magenta tone
6014
+ const log = (msg) => { if (verbose) console.log(msg); };
6015
+ const logBlock = (label, body, max = 600) => {
6016
+ if (!verbose) return;
6017
+ const text = String(body ?? '').trim();
6018
+ if (!text) { console.log(`${accent('[consult]')} ${dim(label + ': (empty)')}`); return; }
6019
+ const truncated = text.length > max ? text.slice(0, max) + dim(`\n... (${text.length - max} more chars)`) : text;
6020
+ console.log(`${accent('[consult]')} ${chalk.white(label)}:\n${dim(' ' + truncated.split('\\n').join('\n '))}`);
6021
+ };
6022
+
6023
+ // Show what we are about to send to the consultant
6024
+ log(`${accent('[consult]')} ${chalk.white('request')} ${dim('to')} ${chalk.white(cfg.model)}`);
6025
+ logBlock('goal', goal);
6026
+ logBlock('question', question);
6027
+ logBlock('summary', summaryText, 800);
6028
+ if (attempts) logBlock('attempts', attempts);
6029
+ if (hints) logBlock('hints', hints);
6030
+ if (attachments.length > 0) {
6031
+ log(`${accent('[consult]')} ${chalk.white('files')} ${dim(`(${attachments.length}, ${formatBytes(totalBytes)})`)}:`);
6032
+ for (const a of attachments) {
6033
+ log(` ${a.error ? chalk.red('!') : chalk.green('+')} ${chalk.white(a.path)}${a.error ? dim(` -- ${a.error}`) : ''}`);
6034
+ }
6035
+ } else {
6036
+ log(`${accent('[consult]')} ${dim('files: none attached \u2014 consultant must rely on its own tools')}`);
6037
+ }
6038
+
5904
6039
  const consultantSpinner = ora(chalk.magenta(`Consultant (${cfg.model.split(':')[0]}) is thinking...`)).start();
5905
6040
  let finalAnswer = '';
5906
6041
  let rounds = 0;
@@ -5908,6 +6043,7 @@ async function consultExpert({ summary, question, goal, attempts, hints, files }
5908
6043
  while (true) {
5909
6044
  if (Date.now() > deadline) {
5910
6045
  consultantSpinner.stop();
6046
+ log(`${chalk.red('[consult]')} timeout hit at ${Math.round((Date.now() - startedAt) / 1000)}s`);
5911
6047
  return `Consultant aborted: hit timeout of ${Math.round(cfg.timeoutMs / 1000)}s before finishing.\n\nPartial answer:\n${finalAnswer || '(none produced yet)'}`;
5912
6048
  }
5913
6049
 
@@ -5923,12 +6059,13 @@ async function consultExpert({ summary, question, goal, attempts, hints, files }
5923
6059
  if (cfg.thinking === 'on') chatOpts.think = true;
5924
6060
  if (useTools && consultantNativeTools.length) chatOpts.tools = consultantNativeTools;
5925
6061
 
6062
+ const roundStart = Date.now();
5926
6063
  let resp;
5927
6064
  try {
5928
6065
  resp = await ollama.chat(chatOpts);
5929
6066
  } catch (err) {
5930
- const msg = err?.message || String(err);
5931
- if (/does not support thinking/i.test(msg) && chatOpts.think) {
6067
+ const errMsg = err?.message || String(err);
6068
+ if (/does not support thinking/i.test(errMsg) && chatOpts.think) {
5932
6069
  delete chatOpts.think;
5933
6070
  resp = await ollama.chat(chatOpts);
5934
6071
  } else {
@@ -5938,7 +6075,16 @@ async function consultExpert({ summary, question, goal, attempts, hints, files }
5938
6075
 
5939
6076
  const msg = resp?.message || {};
5940
6077
  const toolCalls = Array.isArray(msg.tool_calls) ? msg.tool_calls : [];
5941
- finalAnswer = msg.content || finalAnswer;
6078
+ const replyContent = msg.content || '';
6079
+ const replyThinking = msg.thinking || '';
6080
+ finalAnswer = replyContent || finalAnswer;
6081
+
6082
+ consultantSpinner.stop();
6083
+ if (replyThinking) logBlock(`round ${rounds} thinking (${Math.round((Date.now() - roundStart) / 1000)}s)`, replyThinking, 400);
6084
+ if (replyContent) logBlock(`round ${rounds} content`, replyContent, 1200);
6085
+ if (toolCalls.length > 0) {
6086
+ log(`${accent('[consult]')} ${chalk.white(`round ${rounds} \u2192 ${toolCalls.length} tool call${toolCalls.length === 1 ? '' : 's'}`)}`);
6087
+ }
5942
6088
 
5943
6089
  if (toolCalls.length === 0 || rounds >= cfg.toolRoundLimit) {
5944
6090
  break;
@@ -5947,31 +6093,49 @@ async function consultExpert({ summary, question, goal, attempts, hints, files }
5947
6093
  // Push assistant message with tool_calls
5948
6094
  consultMessages.push({ role: 'assistant', content: msg.content || '', tool_calls: toolCalls });
5949
6095
  rounds++;
5950
- consultantSpinner.text = chalk.magenta(`Consultant running tool round ${rounds}/${cfg.toolRoundLimit}...`);
6096
+ consultantSpinner.start(chalk.magenta(`Consultant running tool round ${rounds}/${cfg.toolRoundLimit}...`));
5951
6097
 
5952
6098
  for (const tc of toolCalls) {
5953
6099
  const fn = tc.function || {};
5954
6100
  const args = fn.arguments || {};
5955
6101
  const mapped = normalizeToolName(fn.name || '');
6102
+ const argPreview = (() => {
6103
+ try {
6104
+ const keys = Object.keys(args || {});
6105
+ if (keys.length === 0) return '';
6106
+ return keys.map(k => `${k}=${ellipsis(String(args[k] ?? ''), 60)}`).join(', ');
6107
+ } catch { return ''; }
6108
+ })();
6109
+ consultantSpinner.stop();
6110
+ log(` ${accent('\u2192')} ${chalk.white(fn.name)}${argPreview ? dim('(' + argPreview + ')') : ''}`);
5956
6111
  if (!allowedSet.has(mapped)) {
6112
+ log(` ${chalk.red('blocked')} ${dim('not in allowed tools')}`);
5957
6113
  consultMessages.push({ role: 'tool', tool_name: fn.name, content: `Tool ${fn.name} is not allowed for the consultant (read-only restriction).` });
6114
+ consultantSpinner.start(chalk.magenta(`Consultant running tool round ${rounds}/${cfg.toolRoundLimit}...`));
5958
6115
  continue;
5959
6116
  }
5960
6117
  let toolResult;
6118
+ const tStart = Date.now();
5961
6119
  try {
5962
6120
  toolResult = await runConsultantToolCall(fn.name, args);
5963
6121
  } catch (err) {
5964
6122
  toolResult = `Error: ${err.message}`;
5965
6123
  }
6124
+ const tMs = Date.now() - tStart;
6125
+ const resultStr = String(toolResult ?? '');
6126
+ const isErr = /^error/i.test(resultStr.trim());
6127
+ log(` ${isErr ? chalk.red('err') : chalk.green('ok')} ${dim(`${tMs}ms, ${resultStr.length} chars`)}: ${dim(ellipsis(resultStr.replace(/\s+/g, ' '), 200))}`);
5966
6128
  consultMessages.push({
5967
6129
  role: 'tool',
5968
6130
  tool_name: fn.name,
5969
- content: truncateToolText(String(toolResult ?? ''), 16000),
6131
+ content: truncateToolText(resultStr, 16000),
5970
6132
  });
6133
+ consultantSpinner.start(chalk.magenta(`Consultant running tool round ${rounds}/${cfg.toolRoundLimit}...`));
5971
6134
  }
5972
6135
  }
5973
6136
  } catch (err) {
5974
6137
  consultantSpinner.stop();
6138
+ log(`${chalk.red('[consult]')} error during consultation: ${err.message}`);
5975
6139
  return `Error during consultation: ${err.message}`;
5976
6140
  }
5977
6141
  consultantSpinner.stop();
@@ -5979,6 +6143,11 @@ async function consultExpert({ summary, question, goal, attempts, hints, files }
5979
6143
  const elapsed = Math.round((Date.now() - startedAt) / 1000);
5980
6144
  const answer = (finalAnswer || '').trim() || '(consultant returned no content)';
5981
6145
 
6146
+ // Final summary box
6147
+ log('');
6148
+ log(`${accent('[consult]')} ${chalk.white('final answer')} ${dim(`(${elapsed}s, ${rounds} tool round${rounds === 1 ? '' : 's'})`)}:`);
6149
+ logBlock('answer', answer, 2000);
6150
+
5982
6151
  // Save transcript for audit
5983
6152
  const transcriptBody = [
5984
6153
  `# Consultation Transcript`,
@@ -6001,6 +6170,292 @@ async function consultExpert({ summary, question, goal, attempts, hints, files }
6001
6170
  return `${header}\n\n${answer}`;
6002
6171
  }
6003
6172
 
6173
+ // ─────────────────────────────────────────────────────────────────
6174
+ // DEEP-THINK TOOL — multi-turn back-and-forth with a reasoning model
6175
+ // Each call extends an ongoing thought session so the main agent can
6176
+ // have a real conversation with the thinker (ask, refine, follow up).
6177
+ // Session state lives in memory and is evicted on idle timeout or LRU.
6178
+ // ─────────────────────────────────────────────────────────────────
6179
+
6180
+ const DEFAULT_DEEPTHINK_SYSTEM_PROMPT = `You are a deep-reasoning partner for a junior coding agent. The agent is stuck or facing something unexpected and wants to think out loud with you across multiple turns.
6181
+
6182
+ Your job each turn:
6183
+ - Respond with substantive reasoning, not pleasantries.
6184
+ - Identify what the agent is actually trying to figure out, separate from what they think the problem is.
6185
+ - Walk through the relevant logic, edge cases, alternatives, and tradeoffs.
6186
+ - End with a concrete next thought, suggestion, or clarifying question — something the agent can act on or push back against.
6187
+ - Stay focused. Each turn should build on the previous one. Do not restart the analysis from scratch every turn.
6188
+
6189
+ You have a small set of READ-ONLY tools (read_file, read_chunk, list_directory, search_files, grep, find, regex_search, head, tail, cat, pwd, changes, fetch_web, recall_memory, search_memory_notes, read_memory_notes — exact set depends on configuration). Use them sparingly when the agent's message references code you need to verify. Do not over-explore: only read what is necessary to answer the current turn. You cannot write, patch, run shell commands, or modify state.
6190
+
6191
+ Be direct. Skip filler like "great question!" or "let me think about this...". Just think.`;
6192
+
6193
+ const deepThinkSessions = new Map(); // session_id -> { id, model, messages, turn, startedAt, lastUsedAt }
6194
+ let _deepThinkCounter = 0;
6195
+
6196
+ function pruneIdleDeepThinkSessions(cfg) {
6197
+ const now = Date.now();
6198
+ for (const [sid, s] of deepThinkSessions) {
6199
+ if (now - s.lastUsedAt > cfg.sessionIdleTimeoutMs) {
6200
+ deepThinkSessions.delete(sid);
6201
+ }
6202
+ }
6203
+ }
6204
+
6205
+ function evictOldestDeepThinkSession() {
6206
+ let oldestSid = null;
6207
+ let oldestTs = Infinity;
6208
+ for (const [sid, s] of deepThinkSessions) {
6209
+ if (s.lastUsedAt < oldestTs) { oldestTs = s.lastUsedAt; oldestSid = sid; }
6210
+ }
6211
+ if (oldestSid) deepThinkSessions.delete(oldestSid);
6212
+ }
6213
+
6214
+ function saveDeepThinkTranscript(cfg, session) {
6215
+ if (!cfg.saveTranscripts) return null;
6216
+ try {
6217
+ const dir = isAbsolute(cfg.transcriptDir)
6218
+ ? cfg.transcriptDir
6219
+ : pathResolve(getToolWorkingDirectory(), cfg.transcriptDir);
6220
+ fs.mkdirSync(dir, { recursive: true });
6221
+ const file = join(dir, `${session.id}.md`);
6222
+ const lines = [
6223
+ `# Deep-Think Session ${session.id}`,
6224
+ `- model: ${session.model}`,
6225
+ `- started: ${new Date(session.startedAt).toISOString()}`,
6226
+ `- last used: ${new Date(session.lastUsedAt).toISOString()}`,
6227
+ `- turns so far: ${session.turn}`,
6228
+ ``,
6229
+ ];
6230
+ for (const m of session.messages) {
6231
+ if (m.role === 'system') continue;
6232
+ lines.push(`## ${m.role}`);
6233
+ if (m.thinking) lines.push(`> _thinking_\n>\n> ${m.thinking.split('\n').join('\n> ')}\n`);
6234
+ lines.push(m.content || '(empty)');
6235
+ lines.push('');
6236
+ }
6237
+ fs.writeFileSync(file, lines.join('\n'));
6238
+ return file;
6239
+ } catch { return null; }
6240
+ }
6241
+
6242
+ async function deepThink({ message, session_id, new_session, system_override } = {}) {
6243
+ const cfg = getDeepthinkConfig();
6244
+ if (!cfg.enabled) {
6245
+ return 'Deep-think tool is disabled (deepthink.enabled = false in .sapper/config.json). Enable it before calling deep_think.';
6246
+ }
6247
+
6248
+ const trimmedMessage = String(message ?? '').trim();
6249
+ if (!trimmedMessage) {
6250
+ return 'Error invoking deep_think: \'message\' is required. Pass the question, thought, or context you want to reason about.';
6251
+ }
6252
+ if (trimmedMessage.length > cfg.maxMessageChars) {
6253
+ return `Error invoking deep_think: message is ${trimmedMessage.length} chars (max ${cfg.maxMessageChars}). Trim it or split into multiple turns.`;
6254
+ }
6255
+
6256
+ pruneIdleDeepThinkSessions(cfg);
6257
+
6258
+ let session;
6259
+ const wantsNew = !!new_session;
6260
+ let sid = String(session_id || '').trim();
6261
+ if (sid && !wantsNew && deepThinkSessions.has(sid)) {
6262
+ session = deepThinkSessions.get(sid);
6263
+ if (session.turn >= cfg.maxTurnsPerSession) {
6264
+ return `Deep-think session ${sid} reached max ${cfg.maxTurnsPerSession} turns. Start a new session by calling deep_think with new_session: true and no session_id.`;
6265
+ }
6266
+ } else {
6267
+ if (deepThinkSessions.size >= cfg.maxSessions) {
6268
+ evictOldestDeepThinkSession();
6269
+ }
6270
+ _deepThinkCounter++;
6271
+ sid = `think-${Date.now().toString(36)}-${_deepThinkCounter}`;
6272
+ const systemPrompt = (typeof system_override === 'string' && system_override.trim())
6273
+ ? system_override.trim()
6274
+ : ((cfg.systemPrompt || '').trim() || DEFAULT_DEEPTHINK_SYSTEM_PROMPT);
6275
+ session = {
6276
+ id: sid,
6277
+ model: cfg.model,
6278
+ messages: [{ role: 'system', content: systemPrompt }],
6279
+ turn: 0,
6280
+ startedAt: Date.now(),
6281
+ lastUsedAt: Date.now(),
6282
+ };
6283
+ deepThinkSessions.set(sid, session);
6284
+ }
6285
+
6286
+ session.messages.push({ role: 'user', content: trimmedMessage });
6287
+ session.turn++;
6288
+ session.lastUsedAt = Date.now();
6289
+
6290
+ // Verbose logging
6291
+ const verbose = cfg.verbose !== false;
6292
+ const dim = chalk.gray;
6293
+ const accent = chalk.hex('#b7b9ff');
6294
+ const log = (m) => { if (verbose) console.log(m); };
6295
+ const logBlock = (label, body, max = 600) => {
6296
+ if (!verbose) return;
6297
+ const text = String(body ?? '').trim();
6298
+ if (!text) { console.log(`${accent('[think]')} ${dim(label + ': (empty)')}`); return; }
6299
+ const truncated = text.length > max ? text.slice(0, max) + dim(`\n... (${text.length - max} more chars)`) : text;
6300
+ console.log(`${accent('[think]')} ${chalk.white(label)}:\n${dim(' ' + truncated.split('\n').join('\n '))}`);
6301
+ };
6302
+
6303
+ console.log();
6304
+ console.log(box(
6305
+ `${keyValue('session', chalk.white(sid), 11)}\n` +
6306
+ `${keyValue('model', chalk.white(session.model), 11)}\n` +
6307
+ `${keyValue('turn', chalk.white(`${session.turn} / ${cfg.maxTurnsPerSession}`), 11)} ${UI.slate('·')} ${UI.slate(`per-turn timeout ${Math.round(cfg.perTurnTimeoutMs / 1000)}s`)}\n` +
6308
+ `${keyValue('tools', chalk.white(cfg.tools && cfg.tools.length ? `${cfg.tools.length} read-only · max ${cfg.toolRoundLimit} rounds` : 'disabled (pure reasoning)'), 11)}`,
6309
+ 'Deep Think', 'magenta'
6310
+ ));
6311
+ logBlock(`turn ${session.turn} message`, trimmedMessage, 1200);
6312
+
6313
+ // ── Detect tool capability and build the allowed read-only tool defs ──
6314
+ const allowedSet = new Set((cfg.tools || []).map(normalizeToolName));
6315
+ let useTools = false;
6316
+ let thinkerNativeTools = [];
6317
+ if (allowedSet.size > 0 && cfg.toolRoundLimit > 0) {
6318
+ try {
6319
+ const info = await ollama.show({ model: session.model });
6320
+ useTools = !!(info?.capabilities && info.capabilities.includes('tools'));
6321
+ } catch { useTools = false; }
6322
+ if (useTools) thinkerNativeTools = buildConsultantNativeTools(allowedSet);
6323
+ }
6324
+
6325
+ const start = Date.now();
6326
+ const spinner = ora(chalk.magenta(`Deep-think turn ${session.turn}...`)).start();
6327
+
6328
+ const callOllama = async (messagesForCall, withThink) => {
6329
+ const chatOpts = {
6330
+ model: session.model,
6331
+ messages: messagesForCall,
6332
+ stream: false,
6333
+ options: {
6334
+ temperature: cfg.temperature,
6335
+ ...(cfg.contextLimit ? { num_ctx: cfg.contextLimit } : {}),
6336
+ },
6337
+ };
6338
+ if (withThink && cfg.thinking === 'on') chatOpts.think = true;
6339
+ if (useTools && thinkerNativeTools.length) chatOpts.tools = thinkerNativeTools;
6340
+ return Promise.race([
6341
+ ollama.chat(chatOpts),
6342
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`per-turn timeout ${cfg.perTurnTimeoutMs}ms exceeded`)), cfg.perTurnTimeoutMs)),
6343
+ ]);
6344
+ };
6345
+
6346
+ let resp;
6347
+ let toolRounds = 0;
6348
+ let aggregatedThinking = '';
6349
+ try {
6350
+ while (true) {
6351
+ try {
6352
+ resp = await callOllama(session.messages, true);
6353
+ } catch (err) {
6354
+ const msg = err?.message || String(err);
6355
+ if (/does not support thinking/i.test(msg)) {
6356
+ resp = await callOllama(session.messages, false);
6357
+ } else {
6358
+ throw err;
6359
+ }
6360
+ }
6361
+
6362
+ const reply = resp?.message || {};
6363
+ const toolCalls = Array.isArray(reply.tool_calls) ? reply.tool_calls : [];
6364
+ if (reply.thinking) aggregatedThinking += (aggregatedThinking ? '\n\n' : '') + reply.thinking;
6365
+
6366
+ if (toolCalls.length === 0 || toolRounds >= cfg.toolRoundLimit || !useTools) {
6367
+ break;
6368
+ }
6369
+
6370
+ // Push the assistant turn that requested tools, then run each tool and feed results back
6371
+ session.messages.push({ role: 'assistant', content: reply.content || '', tool_calls: toolCalls });
6372
+ toolRounds++;
6373
+ spinner.text = chalk.magenta(`Deep-think turn ${session.turn} · tool round ${toolRounds}/${cfg.toolRoundLimit}...`);
6374
+
6375
+ for (const tc of toolCalls) {
6376
+ const fn = tc.function || {};
6377
+ const args = fn.arguments || {};
6378
+ const mapped = normalizeToolName(fn.name || '');
6379
+ const argPreview = (() => {
6380
+ try {
6381
+ const keys = Object.keys(args || {});
6382
+ if (keys.length === 0) return '';
6383
+ return keys.map(k => `${k}=${ellipsis(String(args[k] ?? ''), 50)}`).join(', ');
6384
+ } catch { return ''; }
6385
+ })();
6386
+ spinner.stop();
6387
+ log(` ${accent('→')} ${chalk.white(fn.name)}${argPreview ? dim('(' + argPreview + ')') : ''}`);
6388
+ if (!allowedSet.has(mapped)) {
6389
+ log(` ${chalk.red('blocked')} ${dim('not in allowed tools')}`);
6390
+ session.messages.push({ role: 'tool', tool_name: fn.name, content: `Tool ${fn.name} is not allowed for the deep-think reasoner.` });
6391
+ spinner.start(chalk.magenta(`Deep-think turn ${session.turn} · tool round ${toolRounds}/${cfg.toolRoundLimit}...`));
6392
+ continue;
6393
+ }
6394
+ let toolResult;
6395
+ const tStart = Date.now();
6396
+ try {
6397
+ toolResult = await runConsultantToolCall(fn.name, args);
6398
+ } catch (err) {
6399
+ toolResult = `Error: ${err.message}`;
6400
+ }
6401
+ const tMs = Date.now() - tStart;
6402
+ const resultStr = String(toolResult ?? '');
6403
+ const isErr = /^error/i.test(resultStr.trim());
6404
+ log(` ${isErr ? chalk.red('err') : chalk.green('ok')} ${dim(`${tMs}ms, ${resultStr.length} chars`)}: ${dim(ellipsis(resultStr.replace(/\s+/g, ' '), 200))}`);
6405
+ session.messages.push({
6406
+ role: 'tool',
6407
+ tool_name: fn.name,
6408
+ content: truncateToolText(resultStr, 12000),
6409
+ });
6410
+ spinner.start(chalk.magenta(`Deep-think turn ${session.turn} · tool round ${toolRounds}/${cfg.toolRoundLimit}...`));
6411
+ }
6412
+ }
6413
+ } catch (err) {
6414
+ spinner.stop();
6415
+ const msg = err?.message || String(err);
6416
+ // Roll back the user message and any half-pushed assistant/tool entries so the session stays usable
6417
+ while (session.messages.length > 0 && session.messages[session.messages.length - 1].role !== 'user') {
6418
+ session.messages.pop();
6419
+ }
6420
+ if (session.messages.length > 0 && session.messages[session.messages.length - 1].role === 'user') {
6421
+ session.messages.pop();
6422
+ }
6423
+ session.turn--;
6424
+ log(`${chalk.red('[think]')} error: ${msg}`);
6425
+ return `Deep-think error on session ${sid}: ${msg}`;
6426
+ }
6427
+ spinner.stop();
6428
+
6429
+ const replyMessage = resp?.message || {};
6430
+ const content = replyMessage.content || '';
6431
+ const thinking = aggregatedThinking || replyMessage.thinking || '';
6432
+ const elapsed = Math.round((Date.now() - start) / 1000);
6433
+
6434
+ session.messages.push({
6435
+ role: 'assistant',
6436
+ content,
6437
+ ...(thinking ? { thinking } : {}),
6438
+ });
6439
+ session.lastUsedAt = Date.now();
6440
+
6441
+ if (thinking) logBlock(`turn ${session.turn} reasoning (${elapsed}s)`, thinking, 800);
6442
+ if (toolRounds > 0) log(`${accent('[think]')} ${dim(`turn ${session.turn} ran ${toolRounds} tool round${toolRounds === 1 ? '' : 's'}`)}`);
6443
+ logBlock(`turn ${session.turn} reply`, content, 1500);
6444
+
6445
+ const transcriptPath = saveDeepThinkTranscript(cfg, session);
6446
+ const remaining = cfg.maxTurnsPerSession - session.turn;
6447
+ const transcriptNote = transcriptPath
6448
+ ? ` · transcript: ${relative(getToolWorkingDirectory(), transcriptPath) || transcriptPath}`
6449
+ : '';
6450
+
6451
+ const header = `Deep-think session ${sid} · turn ${session.turn}/${cfg.maxTurnsPerSession} · ${elapsed}s${transcriptNote}`;
6452
+ const continuation = remaining > 0
6453
+ ? `\n\nTo continue this thought, call deep_think again with session_id="${sid}" and a follow-up message. ${remaining} turn${remaining === 1 ? '' : 's'} remaining.`
6454
+ : `\n\nThis was the final turn for this session. Start a fresh thought with new_session: true if you need to keep reasoning.`;
6455
+
6456
+ return `${header}\n\n${content || '(no content returned)'}${continuation}`;
6457
+ }
6458
+
6004
6459
  const tools = {
6005
6460
  read: (path) => {
6006
6461
  const trimmedPath = typeof path === 'string' ? path.trim() : '';
@@ -6552,7 +7007,8 @@ const tools = {
6552
7007
  });
6553
7008
  });
6554
7009
  },
6555
- consult: async (args) => consultExpert(args || {})
7010
+ consult: async (args) => consultExpert(args || {}),
7011
+ deepthink: async (args) => deepThink(args || {})
6556
7012
  };
6557
7013
 
6558
7014
  async function checkForUpdates() {
@@ -7164,6 +7620,22 @@ async function runSapper() {
7164
7620
  required: ['goal', 'question', 'summary']
7165
7621
  }
7166
7622
  }
7623
+ },
7624
+ {
7625
+ type: 'function',
7626
+ function: {
7627
+ name: 'deep_think',
7628
+ description: 'Have a multi-turn back-and-forth with a separate reasoning model . Use when you hit something unexpected, need deep reasoning, or want to think out loud with a smarter model. Each call extends an ongoing thought session if you pass session_id; otherwise a new session starts. Sessions cap at a configured max turns. The thinker has NO tools \u2014 it reasons from the message you give it. Include relevant code/context inline. Returns the thinker\\u2019s reply plus the session_id to use for the next turn.',
7629
+ parameters: {
7630
+ type: 'object',
7631
+ properties: {
7632
+ message: { type: 'string', description: 'The question, thought, or context to reason about. The thinker has read-only tools, so referencing paths and what to inspect is enough — include errors, constraints, or short code snippets when they matter.' },
7633
+ session_id: { type: 'string', description: 'Optional. Pass the session_id from a previous deep_think reply to continue the same conversation. Omit to start a new session.' },
7634
+ new_session: { type: 'boolean', description: 'Optional. Set true to force a new thought session even if session_id is provided.' }
7635
+ },
7636
+ required: ['message']
7637
+ }
7638
+ }
7167
7639
  }
7168
7640
  ];
7169
7641
 
@@ -8283,13 +8755,181 @@ async function runSapper() {
8283
8755
  }
8284
8756
  if (subcommand === 'transcripts' || subcommand === 'transcript') {
8285
8757
  if (['on', 'true', 'yes'].includes(value.toLowerCase())) { updateConsultant({ saveTranscripts: true }); console.log(chalk.green('Consultant transcripts: on')); continue; }
8286
- if (['off', 'false', 'no'].includes(value.toLowerCase())) { updateConsultant({ saveTranscripts: false }); console.log(chalk.yellow('Consultant transcripts: off')); continue; }
8287
- console.log(chalk.yellow('Usage: /consult transcripts on|off'));
8758
+ if (['off', 'false', 'no'].includes(value.toLowerCase())) { updateConsultant({ saveTranscripts: false }); console.log(chalk.yellow('Consultant transcripts: off')); continue; } console.log(chalk.yellow('Usage: /consult transcripts on|off'));
8759
+ continue;
8760
+ }
8761
+ if (subcommand === 'verbose' || subcommand === 'log' || subcommand === 'logging') {
8762
+ if (['on', 'true', 'yes', '1'].includes(value.toLowerCase())) { updateConsultant({ verbose: true }); console.log(chalk.green('Consultant verbose logging: on')); continue; }
8763
+ if (['off', 'false', 'no', '0'].includes(value.toLowerCase())) { updateConsultant({ verbose: false }); console.log(chalk.yellow('Consultant verbose logging: off')); continue; }
8764
+ console.log(chalk.yellow('Usage: /consult verbose on|off'));
8288
8765
  continue;
8289
8766
  }
8290
8767
 
8291
8768
  console.log(chalk.yellow(`Unknown consult option: ${subcommand}`));
8292
- console.log(chalk.gray(' Usage: /consult | /consult model <name> | /consult on|off | /consult tools <a,b,c> | /consult minwords <N> | /consult rounds <N> | /consult timeout <secs> | /consult thinking <auto|on|off> | /consult temp <0..2> | /consult transcripts on|off | /consult reset'));
8769
+ console.log(chalk.gray(' Usage: /consult | /consult model <name> | /consult on|off | /consult tools <a,b,c> | /consult minwords <N> | /consult rounds <N> | /consult timeout <secs> | /consult thinking <auto|on|off> | /consult temp <0..2> | /consult transcripts on|off | /consult verbose on|off | /consult reset'));
8770
+ continue;
8771
+ }
8772
+
8773
+ if (input.toLowerCase() === '/think' || input.toLowerCase().startsWith('/think ')) {
8774
+ const arg = input.substring(6).trim();
8775
+ const cfg = getDeepthinkConfig();
8776
+ const updateThink = (patch) => {
8777
+ sapperConfig.deepthink = { ...getDeepthinkConfig(), ...patch };
8778
+ saveConfig(sapperConfig);
8779
+ };
8780
+
8781
+ if (!arg || ['status', 'show'].includes(arg.toLowerCase())) {
8782
+ const liveSessions = Array.from(deepThinkSessions.values()).sort((a, b) => b.lastUsedAt - a.lastUsedAt);
8783
+ const lines = [
8784
+ `enabled ${chalk.white(cfg.enabled ? 'on' : 'off')}`,
8785
+ `model ${chalk.white(cfg.model)}`,
8786
+ `loop ${UI.slate(`max ${cfg.maxTurnsPerSession} turns/session \u00b7 max ${cfg.maxSessions} sessions \u00b7 idle ${Math.round(cfg.sessionIdleTimeoutMs/60000)}m \u00b7 per-turn ${Math.round(cfg.perTurnTimeoutMs/1000)}s`)}`,
8787
+ `reasoning ${UI.slate(`thinking ${cfg.thinking} \u00b7 temp ${cfg.temperature} \u00b7 max msg ${cfg.maxMessageChars} chars`)}`,
8788
+ `tools ${chalk.white(cfg.tools && cfg.tools.length ? `${cfg.tools.length} read-only` : 'disabled')} ${UI.slate('\u00b7')} ${UI.slate(`max ${cfg.toolRoundLimit} rounds/turn`)}`,
8789
+ ` ${UI.slate((cfg.tools || []).join(', ') || '(no tools \u2014 pure reasoning from the message)')}`,
8790
+ `transcripts ${chalk.white(cfg.saveTranscripts ? 'on' : 'off')} ${UI.slate('\u2192')} ${UI.slate(cfg.transcriptDir)}`,
8791
+ `verbose ${chalk.white(cfg.verbose ? 'on' : 'off')}`,
8792
+ `live sessions ${chalk.white(`${liveSessions.length}`)}`,
8793
+ ];
8794
+ for (const s of liveSessions.slice(0, 5)) {
8795
+ const ageMin = Math.round((Date.now() - s.lastUsedAt) / 60000);
8796
+ lines.push(` ${chalk.white(s.id)} ${UI.slate('\u00b7')} ${UI.slate(`turn ${s.turn}/${cfg.maxTurnsPerSession}`)} ${UI.slate('\u00b7')} ${UI.slate(`idle ${ageMin}m`)}`);
8797
+ }
8798
+ lines.push('');
8799
+ lines.push(UI.slate('Usage: /think model <name> | /think on|off | /think turns <N> | /think sessions <N>'));
8800
+ lines.push(UI.slate(' /think idle <minutes> | /think timeout <secs> | /think thinking <auto|on|off>'));
8801
+ lines.push(UI.slate(' /think temp <0..2> | /think maxmsg <chars> | /think tools <a,b,c | none>'));
8802
+ lines.push(UI.slate(' /think toolrounds <N> | /think transcripts <on|off> | /think verbose <on|off>'));
8803
+ lines.push(UI.slate(' /think list | /think clear [<id>] | /think reset'));
8804
+ console.log();
8805
+ console.log(box(lines.join('\n'), 'Deep Think', 'magenta'));
8806
+ continue;
8807
+ }
8808
+
8809
+ const [subcommandRaw, ...rest] = arg.split(/\s+/);
8810
+ const subcommand = subcommandRaw.toLowerCase();
8811
+ const value = rest.join(' ').trim();
8812
+
8813
+ if (subcommand === 'reset' || subcommand === 'default') {
8814
+ sapperConfig.deepthink = { ...DEFAULT_CONFIG.deepthink };
8815
+ saveConfig(sapperConfig);
8816
+ console.log(chalk.green('Deep-think settings reset to defaults.'));
8817
+ continue;
8818
+ }
8819
+ if (['on', 'true', 'yes', 'enable', 'enabled'].includes(subcommand)) { updateThink({ enabled: true }); console.log(chalk.green('Deep-think enabled.')); continue; }
8820
+ if (['off', 'false', 'no', 'disable', 'disabled'].includes(subcommand)) { updateThink({ enabled: false }); console.log(chalk.yellow('Deep-think disabled.')); continue; }
8821
+ if (subcommand === 'model') {
8822
+ if (!value) { console.log(chalk.yellow('Usage: /think model <name>')); continue; }
8823
+ updateThink({ model: value });
8824
+ console.log(chalk.green(`Deep-think model set to ${value}.`));
8825
+ continue;
8826
+ }
8827
+ if (subcommand === 'turns' || subcommand === 'maxturns') {
8828
+ const n = parseInt(value, 10);
8829
+ if (!Number.isFinite(n)) { console.log(chalk.yellow('Usage: /think turns <N>')); continue; }
8830
+ updateThink({ maxTurnsPerSession: n });
8831
+ console.log(chalk.green(`Deep-think max turns per session: ${getDeepthinkConfig().maxTurnsPerSession}`));
8832
+ continue;
8833
+ }
8834
+ if (subcommand === 'sessions' || subcommand === 'maxsessions') {
8835
+ const n = parseInt(value, 10);
8836
+ if (!Number.isFinite(n)) { console.log(chalk.yellow('Usage: /think sessions <N>')); continue; }
8837
+ updateThink({ maxSessions: n });
8838
+ console.log(chalk.green(`Deep-think max concurrent sessions: ${getDeepthinkConfig().maxSessions}`));
8839
+ continue;
8840
+ }
8841
+ if (subcommand === 'idle') {
8842
+ const n = parseInt(value, 10);
8843
+ if (!Number.isFinite(n)) { console.log(chalk.yellow('Usage: /think idle <minutes>')); continue; }
8844
+ updateThink({ sessionIdleTimeoutMs: n * 60000 });
8845
+ console.log(chalk.green(`Deep-think idle timeout: ${n}m`));
8846
+ continue;
8847
+ }
8848
+ if (subcommand === 'timeout') {
8849
+ const n = parseInt(value, 10);
8850
+ if (!Number.isFinite(n)) { console.log(chalk.yellow('Usage: /think timeout <seconds>')); continue; }
8851
+ updateThink({ perTurnTimeoutMs: n * 1000 });
8852
+ console.log(chalk.green(`Deep-think per-turn timeout: ${n}s`));
8853
+ continue;
8854
+ }
8855
+ if (subcommand === 'thinking') {
8856
+ updateThink({ thinking: value });
8857
+ console.log(chalk.green(`Deep-think reasoning mode: ${getDeepthinkConfig().thinking}`));
8858
+ continue;
8859
+ }
8860
+ if (subcommand === 'temp' || subcommand === 'temperature') {
8861
+ const n = Number(value);
8862
+ if (!Number.isFinite(n)) { console.log(chalk.yellow('Usage: /think temp <0..2>')); continue; }
8863
+ updateThink({ temperature: n });
8864
+ console.log(chalk.green(`Deep-think temperature: ${getDeepthinkConfig().temperature}`));
8865
+ continue;
8866
+ }
8867
+ if (subcommand === 'maxmsg' || subcommand === 'msgmax') {
8868
+ const n = parseInt(value, 10);
8869
+ if (!Number.isFinite(n)) { console.log(chalk.yellow('Usage: /think maxmsg <chars>')); continue; }
8870
+ updateThink({ maxMessageChars: n });
8871
+ console.log(chalk.green(`Deep-think max message chars: ${getDeepthinkConfig().maxMessageChars}`));
8872
+ continue;
8873
+ }
8874
+ if (subcommand === 'tools') {
8875
+ if (!value || ['none', 'off', 'disable', 'disabled', '[]'].includes(value.toLowerCase())) {
8876
+ updateThink({ tools: [] });
8877
+ console.log(chalk.yellow('Deep-think tools: disabled (thinker now reasons only from the message)'));
8878
+ continue;
8879
+ }
8880
+ if (['default', 'reset'].includes(value.toLowerCase())) {
8881
+ updateThink({ tools: [...DEFAULT_CONFIG.deepthink.tools] });
8882
+ console.log(chalk.green(`Deep-think tools reset to defaults: ${getDeepthinkConfig().tools.join(', ')}`));
8883
+ continue;
8884
+ }
8885
+ const list = value.split(/[,\s]+/).map(t => t.trim()).filter(Boolean);
8886
+ if (!list.length) { console.log(chalk.yellow('Usage: /think tools read,list,grep,... | /think tools none | /think tools default')); continue; }
8887
+ updateThink({ tools: list });
8888
+ console.log(chalk.green(`Deep-think tools: ${getDeepthinkConfig().tools.join(', ')}`));
8889
+ continue;
8890
+ }
8891
+ if (subcommand === 'toolrounds' || subcommand === 'rounds') {
8892
+ const n = parseInt(value, 10);
8893
+ if (!Number.isFinite(n)) { console.log(chalk.yellow('Usage: /think toolrounds <N> (0 disables tool calls for the thinker)')); continue; }
8894
+ updateThink({ toolRoundLimit: n });
8895
+ console.log(chalk.green(`Deep-think tool-round limit: ${getDeepthinkConfig().toolRoundLimit}`));
8896
+ continue;
8897
+ }
8898
+ if (subcommand === 'transcripts' || subcommand === 'transcript') {
8899
+ if (['on', 'true', 'yes'].includes(value.toLowerCase())) { updateThink({ saveTranscripts: true }); console.log(chalk.green('Deep-think transcripts: on')); continue; }
8900
+ if (['off', 'false', 'no'].includes(value.toLowerCase())) { updateThink({ saveTranscripts: false }); console.log(chalk.yellow('Deep-think transcripts: off')); continue; }
8901
+ console.log(chalk.yellow('Usage: /think transcripts on|off'));
8902
+ continue;
8903
+ }
8904
+ if (subcommand === 'verbose' || subcommand === 'log' || subcommand === 'logging') {
8905
+ if (['on', 'true', 'yes', '1'].includes(value.toLowerCase())) { updateThink({ verbose: true }); console.log(chalk.green('Deep-think verbose logging: on')); continue; }
8906
+ if (['off', 'false', 'no', '0'].includes(value.toLowerCase())) { updateThink({ verbose: false }); console.log(chalk.yellow('Deep-think verbose logging: off')); continue; }
8907
+ console.log(chalk.yellow('Usage: /think verbose on|off'));
8908
+ continue;
8909
+ }
8910
+ if (subcommand === 'list' || subcommand === 'sess' || subcommand === 'ls') {
8911
+ if (deepThinkSessions.size === 0) { console.log(UI.slate('No active deep-think sessions.')); continue; }
8912
+ for (const s of deepThinkSessions.values()) {
8913
+ const ageMin = Math.round((Date.now() - s.lastUsedAt) / 60000);
8914
+ console.log(` ${chalk.white(s.id)} ${UI.slate('\u00b7')} ${UI.slate(`turn ${s.turn}/${cfg.maxTurnsPerSession}`)} ${UI.slate('\u00b7')} ${UI.slate(`idle ${ageMin}m`)}`);
8915
+ }
8916
+ continue;
8917
+ }
8918
+ if (subcommand === 'clear' || subcommand === 'drop' || subcommand === 'end') {
8919
+ if (!value) {
8920
+ const n = deepThinkSessions.size;
8921
+ deepThinkSessions.clear();
8922
+ console.log(chalk.green(`Cleared ${n} deep-think session${n === 1 ? '' : 's'}.`));
8923
+ } else if (deepThinkSessions.delete(value)) {
8924
+ console.log(chalk.green(`Cleared session ${value}.`));
8925
+ } else {
8926
+ console.log(chalk.yellow(`No session named ${value}.`));
8927
+ }
8928
+ continue;
8929
+ }
8930
+
8931
+ console.log(chalk.yellow(`Unknown think option: ${subcommand}`));
8932
+ console.log(chalk.gray(' Usage: /think | /think model <name> | /think on|off | /think turns <N> | /think sessions <N> | /think idle <m> | /think timeout <s> | /think thinking <auto|on|off> | /think temp <0..2> | /think maxmsg <chars> | /think tools <a,b,c | none | default> | /think toolrounds <N> | /think transcripts on|off | /think verbose on|off | /think list | /think clear [<id>] | /think reset'));
8293
8933
  continue;
8294
8934
  }
8295
8935
 
@@ -9629,7 +10269,7 @@ async function runSapper() {
9629
10269
  ls: 'LS', cat: 'CAT', head: 'HEAD', tail: 'TAIL', grep: 'GREP', find: 'FIND',
9630
10270
  pwd: 'PWD', cd: 'CD', rmdir: 'RMDIR', changes: 'CHANGES',
9631
10271
  fetch_web: 'FETCH', recall_memory: 'MEMORY', open_url: 'OPEN', run_shell: 'SHELL',
9632
- consult_expert: 'CONSULT'
10272
+ consult_expert: 'CONSULT', deep_think: 'DEEPTHINK'
9633
10273
  };
9634
10274
 
9635
10275
  showStreamPhase(`Running ${nativeToolCalls.length} native tool call${nativeToolCalls.length === 1 ? '' : 's'}...`);
@@ -9761,6 +10401,10 @@ async function runSapper() {
9761
10401
  result = await tools.consult(args);
9762
10402
  logEntry('tool', { toolType: 'CONSULT', path: (args && args.question ? String(args.question).slice(0, 80) : 'consult'), duration: Date.now() - toolStart, success: !String(result).startsWith('Error'), resultSize: result?.length });
9763
10403
  break;
10404
+ case 'deep_think':
10405
+ result = await tools.deepthink(args);
10406
+ logEntry('tool', { toolType: 'DEEPTHINK', path: (args && args.message ? String(args.message).slice(0, 80) : 'deepthink'), duration: Date.now() - toolStart, success: !String(result).startsWith('Error'), resultSize: result?.length });
10407
+ break;
9764
10408
  default:
9765
10409
  result = `Unknown tool: ${fn.name}`;
9766
10410
  toolSuccess = false;
@@ -10050,9 +10694,37 @@ async function runSapper() {
10050
10694
  result = await tools.consult(consultArgs);
10051
10695
  logEntry('tool', { toolType: 'CONSULT', path: (consultArgs.question || 'consult').slice(0, 80), duration: Date.now() - toolStart, success: !String(result).startsWith('Error'), resultSize: result?.length });
10052
10696
  }
10697
+ else if (type.toLowerCase() === 'deepthink' || type.toLowerCase() === 'think' || type.toLowerCase() === 'talk' || type.toLowerCase() === 'moe') {
10698
+ // Text-marker forms:
10699
+ // [TOOL:THINK]<message>[/TOOL] -> new session
10700
+ // [TOOL:THINK]session_id:::<message>[/TOOL] -> continue session
10701
+ // [TOOL:THINK]{"message":"...","session_id":"..."}[/TOOL]
10702
+ let thinkArgs = null;
10703
+ const raw = (content && content.trim()) ? `${path}:::${content}` : String(path || '');
10704
+ const trimmedRaw = raw.trim();
10705
+ if (trimmedRaw.startsWith('{')) {
10706
+ try { thinkArgs = JSON.parse(trimmedRaw); } catch { thinkArgs = null; }
10707
+ }
10708
+ if (!thinkArgs) {
10709
+ const sepIdx = trimmedRaw.indexOf(':::');
10710
+ if (sepIdx > -1) {
10711
+ const maybeSid = trimmedRaw.slice(0, sepIdx).trim();
10712
+ // Treat the first segment as a session_id only if it looks like one
10713
+ if (/^think-[a-z0-9-]+$/i.test(maybeSid)) {
10714
+ thinkArgs = { session_id: maybeSid, message: trimmedRaw.slice(sepIdx + 3).trim() };
10715
+ } else {
10716
+ thinkArgs = { message: trimmedRaw };
10717
+ }
10718
+ } else {
10719
+ thinkArgs = { message: trimmedRaw };
10720
+ }
10721
+ }
10722
+ result = await tools.deepthink(thinkArgs);
10723
+ logEntry('tool', { toolType: 'DEEPTHINK', path: ((thinkArgs && thinkArgs.message) || 'deepthink').slice(0, 80), duration: Date.now() - toolStart, success: !String(result).startsWith('Error'), resultSize: result?.length });
10724
+ }
10053
10725
 
10054
10726
  // Log tool execution (for non-shell, non-file specific ones)
10055
- if (!['list', 'ls', 'read', 'cat', 'head', 'tail', 'mkdir', 'rmdir', 'pwd', 'cd', 'write', 'patch', 'search', 'grep', 'find', 'regex', 'chunk', 'read_chunk', 'changes', 'fetch', 'memory', 'memory_note_save', 'memory_note_search', 'memory_note_read', 'open', 'shell', 'consult'].includes(type.toLowerCase())) {
10727
+ if (!['list', 'ls', 'read', 'cat', 'head', 'tail', 'mkdir', 'rmdir', 'pwd', 'cd', 'write', 'patch', 'search', 'grep', 'find', 'regex', 'chunk', 'read_chunk', 'changes', 'fetch', 'memory', 'memory_note_save', 'memory_note_search', 'memory_note_read', 'open', 'shell', 'consult', 'deepthink', 'think', 'talk', 'moe'].includes(type.toLowerCase())) {
10056
10728
  logEntry('tool', { toolType: type.toUpperCase(), path, duration: Date.now() - toolStart, success: toolSuccess, resultSize: result?.length, error: toolSuccess ? undefined : result });
10057
10729
  }
10058
10730