sapper-iq 1.4.6 → 1.4.7
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 +1 -1
- package/sapper.mjs +77 -8
package/package.json
CHANGED
package/sapper.mjs
CHANGED
|
@@ -1171,6 +1171,7 @@ const DEFAULT_CONFIG = Object.freeze({
|
|
|
1171
1171
|
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
1172
|
saveTranscripts: true, // Persist every consultation to disk for audit.
|
|
1173
1173
|
transcriptDir: '.sapper/consultations', // Where transcripts are written.
|
|
1174
|
+
verbose: true, // Print full request, each tool call/result, and final answer to the terminal during the consultation.
|
|
1174
1175
|
systemPrompt: '', // Override the built-in consultant system prompt (empty = use default).
|
|
1175
1176
|
}),
|
|
1176
1177
|
prompt: Object.freeze({
|
|
@@ -1591,6 +1592,7 @@ function normalizeConsultantConfig(consultantConfig = {}) {
|
|
|
1591
1592
|
transcriptDir: typeof consultantConfig.transcriptDir === 'string' && consultantConfig.transcriptDir.trim()
|
|
1592
1593
|
? consultantConfig.transcriptDir.trim()
|
|
1593
1594
|
: D.transcriptDir,
|
|
1595
|
+
verbose: normalizeBoolean(consultantConfig.verbose, D.verbose),
|
|
1594
1596
|
systemPrompt: typeof consultantConfig.systemPrompt === 'string' ? consultantConfig.systemPrompt : D.systemPrompt,
|
|
1595
1597
|
};
|
|
1596
1598
|
}
|
|
@@ -5901,6 +5903,34 @@ async function consultExpert({ summary, question, goal, attempts, hints, files }
|
|
|
5901
5903
|
'Consulting expert', 'magenta'
|
|
5902
5904
|
));
|
|
5903
5905
|
|
|
5906
|
+
const verbose = cfg.verbose !== false;
|
|
5907
|
+
const dim = chalk.gray;
|
|
5908
|
+
const accent = chalk.hex('#b7b9ff'); // magenta tone
|
|
5909
|
+
const log = (msg) => { if (verbose) console.log(msg); };
|
|
5910
|
+
const logBlock = (label, body, max = 600) => {
|
|
5911
|
+
if (!verbose) return;
|
|
5912
|
+
const text = String(body ?? '').trim();
|
|
5913
|
+
if (!text) { console.log(`${accent('[consult]')} ${dim(label + ': (empty)')}`); return; }
|
|
5914
|
+
const truncated = text.length > max ? text.slice(0, max) + dim(`\n... (${text.length - max} more chars)`) : text;
|
|
5915
|
+
console.log(`${accent('[consult]')} ${chalk.white(label)}:\n${dim(' ' + truncated.split('\\n').join('\n '))}`);
|
|
5916
|
+
};
|
|
5917
|
+
|
|
5918
|
+
// Show what we are about to send to the consultant
|
|
5919
|
+
log(`${accent('[consult]')} ${chalk.white('request')} ${dim('to')} ${chalk.white(cfg.model)}`);
|
|
5920
|
+
logBlock('goal', goal);
|
|
5921
|
+
logBlock('question', question);
|
|
5922
|
+
logBlock('summary', summaryText, 800);
|
|
5923
|
+
if (attempts) logBlock('attempts', attempts);
|
|
5924
|
+
if (hints) logBlock('hints', hints);
|
|
5925
|
+
if (attachments.length > 0) {
|
|
5926
|
+
log(`${accent('[consult]')} ${chalk.white('files')} ${dim(`(${attachments.length}, ${formatBytes(totalBytes)})`)}:`);
|
|
5927
|
+
for (const a of attachments) {
|
|
5928
|
+
log(` ${a.error ? chalk.red('!') : chalk.green('+')} ${chalk.white(a.path)}${a.error ? dim(` -- ${a.error}`) : ''}`);
|
|
5929
|
+
}
|
|
5930
|
+
} else {
|
|
5931
|
+
log(`${accent('[consult]')} ${dim('files: none attached \u2014 consultant must rely on its own tools')}`);
|
|
5932
|
+
}
|
|
5933
|
+
|
|
5904
5934
|
const consultantSpinner = ora(chalk.magenta(`Consultant (${cfg.model.split(':')[0]}) is thinking...`)).start();
|
|
5905
5935
|
let finalAnswer = '';
|
|
5906
5936
|
let rounds = 0;
|
|
@@ -5908,6 +5938,7 @@ async function consultExpert({ summary, question, goal, attempts, hints, files }
|
|
|
5908
5938
|
while (true) {
|
|
5909
5939
|
if (Date.now() > deadline) {
|
|
5910
5940
|
consultantSpinner.stop();
|
|
5941
|
+
log(`${chalk.red('[consult]')} timeout hit at ${Math.round((Date.now() - startedAt) / 1000)}s`);
|
|
5911
5942
|
return `Consultant aborted: hit timeout of ${Math.round(cfg.timeoutMs / 1000)}s before finishing.\n\nPartial answer:\n${finalAnswer || '(none produced yet)'}`;
|
|
5912
5943
|
}
|
|
5913
5944
|
|
|
@@ -5923,12 +5954,13 @@ async function consultExpert({ summary, question, goal, attempts, hints, files }
|
|
|
5923
5954
|
if (cfg.thinking === 'on') chatOpts.think = true;
|
|
5924
5955
|
if (useTools && consultantNativeTools.length) chatOpts.tools = consultantNativeTools;
|
|
5925
5956
|
|
|
5957
|
+
const roundStart = Date.now();
|
|
5926
5958
|
let resp;
|
|
5927
5959
|
try {
|
|
5928
5960
|
resp = await ollama.chat(chatOpts);
|
|
5929
5961
|
} catch (err) {
|
|
5930
|
-
const
|
|
5931
|
-
if (/does not support thinking/i.test(
|
|
5962
|
+
const errMsg = err?.message || String(err);
|
|
5963
|
+
if (/does not support thinking/i.test(errMsg) && chatOpts.think) {
|
|
5932
5964
|
delete chatOpts.think;
|
|
5933
5965
|
resp = await ollama.chat(chatOpts);
|
|
5934
5966
|
} else {
|
|
@@ -5938,7 +5970,16 @@ async function consultExpert({ summary, question, goal, attempts, hints, files }
|
|
|
5938
5970
|
|
|
5939
5971
|
const msg = resp?.message || {};
|
|
5940
5972
|
const toolCalls = Array.isArray(msg.tool_calls) ? msg.tool_calls : [];
|
|
5941
|
-
|
|
5973
|
+
const replyContent = msg.content || '';
|
|
5974
|
+
const replyThinking = msg.thinking || '';
|
|
5975
|
+
finalAnswer = replyContent || finalAnswer;
|
|
5976
|
+
|
|
5977
|
+
consultantSpinner.stop();
|
|
5978
|
+
if (replyThinking) logBlock(`round ${rounds} thinking (${Math.round((Date.now() - roundStart) / 1000)}s)`, replyThinking, 400);
|
|
5979
|
+
if (replyContent) logBlock(`round ${rounds} content`, replyContent, 1200);
|
|
5980
|
+
if (toolCalls.length > 0) {
|
|
5981
|
+
log(`${accent('[consult]')} ${chalk.white(`round ${rounds} \u2192 ${toolCalls.length} tool call${toolCalls.length === 1 ? '' : 's'}`)}`);
|
|
5982
|
+
}
|
|
5942
5983
|
|
|
5943
5984
|
if (toolCalls.length === 0 || rounds >= cfg.toolRoundLimit) {
|
|
5944
5985
|
break;
|
|
@@ -5947,31 +5988,49 @@ async function consultExpert({ summary, question, goal, attempts, hints, files }
|
|
|
5947
5988
|
// Push assistant message with tool_calls
|
|
5948
5989
|
consultMessages.push({ role: 'assistant', content: msg.content || '', tool_calls: toolCalls });
|
|
5949
5990
|
rounds++;
|
|
5950
|
-
consultantSpinner.
|
|
5991
|
+
consultantSpinner.start(chalk.magenta(`Consultant running tool round ${rounds}/${cfg.toolRoundLimit}...`));
|
|
5951
5992
|
|
|
5952
5993
|
for (const tc of toolCalls) {
|
|
5953
5994
|
const fn = tc.function || {};
|
|
5954
5995
|
const args = fn.arguments || {};
|
|
5955
5996
|
const mapped = normalizeToolName(fn.name || '');
|
|
5997
|
+
const argPreview = (() => {
|
|
5998
|
+
try {
|
|
5999
|
+
const keys = Object.keys(args || {});
|
|
6000
|
+
if (keys.length === 0) return '';
|
|
6001
|
+
return keys.map(k => `${k}=${ellipsis(String(args[k] ?? ''), 60)}`).join(', ');
|
|
6002
|
+
} catch { return ''; }
|
|
6003
|
+
})();
|
|
6004
|
+
consultantSpinner.stop();
|
|
6005
|
+
log(` ${accent('\u2192')} ${chalk.white(fn.name)}${argPreview ? dim('(' + argPreview + ')') : ''}`);
|
|
5956
6006
|
if (!allowedSet.has(mapped)) {
|
|
6007
|
+
log(` ${chalk.red('blocked')} ${dim('not in allowed tools')}`);
|
|
5957
6008
|
consultMessages.push({ role: 'tool', tool_name: fn.name, content: `Tool ${fn.name} is not allowed for the consultant (read-only restriction).` });
|
|
6009
|
+
consultantSpinner.start(chalk.magenta(`Consultant running tool round ${rounds}/${cfg.toolRoundLimit}...`));
|
|
5958
6010
|
continue;
|
|
5959
6011
|
}
|
|
5960
6012
|
let toolResult;
|
|
6013
|
+
const tStart = Date.now();
|
|
5961
6014
|
try {
|
|
5962
6015
|
toolResult = await runConsultantToolCall(fn.name, args);
|
|
5963
6016
|
} catch (err) {
|
|
5964
6017
|
toolResult = `Error: ${err.message}`;
|
|
5965
6018
|
}
|
|
6019
|
+
const tMs = Date.now() - tStart;
|
|
6020
|
+
const resultStr = String(toolResult ?? '');
|
|
6021
|
+
const isErr = /^error/i.test(resultStr.trim());
|
|
6022
|
+
log(` ${isErr ? chalk.red('err') : chalk.green('ok')} ${dim(`${tMs}ms, ${resultStr.length} chars`)}: ${dim(ellipsis(resultStr.replace(/\s+/g, ' '), 200))}`);
|
|
5966
6023
|
consultMessages.push({
|
|
5967
6024
|
role: 'tool',
|
|
5968
6025
|
tool_name: fn.name,
|
|
5969
|
-
content: truncateToolText(
|
|
6026
|
+
content: truncateToolText(resultStr, 16000),
|
|
5970
6027
|
});
|
|
6028
|
+
consultantSpinner.start(chalk.magenta(`Consultant running tool round ${rounds}/${cfg.toolRoundLimit}...`));
|
|
5971
6029
|
}
|
|
5972
6030
|
}
|
|
5973
6031
|
} catch (err) {
|
|
5974
6032
|
consultantSpinner.stop();
|
|
6033
|
+
log(`${chalk.red('[consult]')} error during consultation: ${err.message}`);
|
|
5975
6034
|
return `Error during consultation: ${err.message}`;
|
|
5976
6035
|
}
|
|
5977
6036
|
consultantSpinner.stop();
|
|
@@ -5979,6 +6038,11 @@ async function consultExpert({ summary, question, goal, attempts, hints, files }
|
|
|
5979
6038
|
const elapsed = Math.round((Date.now() - startedAt) / 1000);
|
|
5980
6039
|
const answer = (finalAnswer || '').trim() || '(consultant returned no content)';
|
|
5981
6040
|
|
|
6041
|
+
// Final summary box
|
|
6042
|
+
log('');
|
|
6043
|
+
log(`${accent('[consult]')} ${chalk.white('final answer')} ${dim(`(${elapsed}s, ${rounds} tool round${rounds === 1 ? '' : 's'})`)}:`);
|
|
6044
|
+
logBlock('answer', answer, 2000);
|
|
6045
|
+
|
|
5982
6046
|
// Save transcript for audit
|
|
5983
6047
|
const transcriptBody = [
|
|
5984
6048
|
`# Consultation Transcript`,
|
|
@@ -8283,13 +8347,18 @@ async function runSapper() {
|
|
|
8283
8347
|
}
|
|
8284
8348
|
if (subcommand === 'transcripts' || subcommand === 'transcript') {
|
|
8285
8349
|
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
|
-
|
|
8350
|
+
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'));
|
|
8351
|
+
continue;
|
|
8352
|
+
}
|
|
8353
|
+
if (subcommand === 'verbose' || subcommand === 'log' || subcommand === 'logging') {
|
|
8354
|
+
if (['on', 'true', 'yes', '1'].includes(value.toLowerCase())) { updateConsultant({ verbose: true }); console.log(chalk.green('Consultant verbose logging: on')); continue; }
|
|
8355
|
+
if (['off', 'false', 'no', '0'].includes(value.toLowerCase())) { updateConsultant({ verbose: false }); console.log(chalk.yellow('Consultant verbose logging: off')); continue; }
|
|
8356
|
+
console.log(chalk.yellow('Usage: /consult verbose on|off'));
|
|
8288
8357
|
continue;
|
|
8289
8358
|
}
|
|
8290
8359
|
|
|
8291
8360
|
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'));
|
|
8361
|
+
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'));
|
|
8293
8362
|
continue;
|
|
8294
8363
|
}
|
|
8295
8364
|
|