prior-cli 1.3.11 → 1.4.0
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/bin/prior.js +94 -34
- package/lib/agent.js +107 -26
- package/package.json +1 -1
package/bin/prior.js
CHANGED
|
@@ -388,7 +388,9 @@ function hyperlink(text, url) {
|
|
|
388
388
|
return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`;
|
|
389
389
|
}
|
|
390
390
|
|
|
391
|
-
|
|
391
|
+
const PREVIEW_TOOLS = new Set(['file_read', 'run_command', 'web_search', 'url_fetch']);
|
|
392
|
+
|
|
393
|
+
function renderToolDone(name, summary, preview) {
|
|
392
394
|
const took = _toolStartTime ? c.dim(` · ${elapsed(Date.now() - _toolStartTime)}`) : '';
|
|
393
395
|
let display = summary || '';
|
|
394
396
|
if (/^[a-zA-Z]:[/\\]/.test(display) || display.startsWith('/')) {
|
|
@@ -398,6 +400,19 @@ function renderToolDone(name, summary) {
|
|
|
398
400
|
display = c.dim(display);
|
|
399
401
|
}
|
|
400
402
|
process.stdout.write(` ${c.ok('✓')} ${c.muted(name)} ${display}${took}\n`);
|
|
403
|
+
|
|
404
|
+
// Rich preview for certain tools
|
|
405
|
+
if (preview && PREVIEW_TOOLS.has(name)) {
|
|
406
|
+
const lines = String(preview).split('\n').filter(l => l.trim());
|
|
407
|
+
const toShow = lines.slice(0, 5);
|
|
408
|
+
const more = lines.length - toShow.length;
|
|
409
|
+
if (toShow.length > 0) {
|
|
410
|
+
drawBox([
|
|
411
|
+
...toShow.map(l => ({ text: l.slice(0, 80), dim: true })),
|
|
412
|
+
...(more > 0 ? [{ text: `… ${more} more line${more !== 1 ? 's' : ''}`, dim: true }] : []),
|
|
413
|
+
]);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
401
416
|
}
|
|
402
417
|
|
|
403
418
|
function renderToolError(name, error) {
|
|
@@ -608,7 +623,7 @@ async function startChat(opts = {}) {
|
|
|
608
623
|
const chatHistory = [];
|
|
609
624
|
let currentModel = opts.model || null;
|
|
610
625
|
let _currentAbortController = null;
|
|
611
|
-
let
|
|
626
|
+
let _pendingImages = []; // set by alt+v clipboard paste (supports multiple)
|
|
612
627
|
|
|
613
628
|
// ── Live slash-command suggestions ──────────────────────────
|
|
614
629
|
let clearSuggestions = () => {};
|
|
@@ -652,8 +667,12 @@ async function startChat(opts = {}) {
|
|
|
652
667
|
let rows = 0;
|
|
653
668
|
|
|
654
669
|
// Image indicator — always first, persists across backspace/typing
|
|
655
|
-
if (
|
|
656
|
-
|
|
670
|
+
if (_pendingImages.length > 0) {
|
|
671
|
+
const tags = _pendingImages.map((_, i) => c.brand(`[Image ${i + 1}]`)).join(' ');
|
|
672
|
+
const hint = _pendingImages.length > 0
|
|
673
|
+
? c.dim(' · alt+v to add more · alt+v (empty clipboard) to remove last')
|
|
674
|
+
: '';
|
|
675
|
+
process.stdout.write(`\x1b[B\r\x1b[2K ${c.brand('◈')} ${tags}${hint}`);
|
|
657
676
|
rows++;
|
|
658
677
|
}
|
|
659
678
|
|
|
@@ -690,24 +709,22 @@ async function startChat(opts = {}) {
|
|
|
690
709
|
return;
|
|
691
710
|
}
|
|
692
711
|
|
|
693
|
-
// Alt+V —
|
|
712
|
+
// Alt+V — add image from clipboard, or remove last if clipboard is empty
|
|
694
713
|
if (key.meta && key.name === 'v') {
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
flashSubRow(c.muted('✗ No image found in clipboard'));
|
|
707
|
-
}
|
|
708
|
-
} catch {
|
|
709
|
-
flashSubRow(c.muted('✗ Could not read clipboard'));
|
|
714
|
+
try {
|
|
715
|
+
const ps = `Add-Type -AssemblyName System.Windows.Forms; $img = [System.Windows.Forms.Clipboard]::GetImage(); if ($img) { $ms = New-Object System.IO.MemoryStream; $img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png); [Convert]::ToBase64String($ms.ToArray()) } else { '' }`;
|
|
716
|
+
const b64 = execSync(`powershell -NoProfile -Command "${ps}"`, { timeout: 5000 }).toString().trim();
|
|
717
|
+
if (b64) {
|
|
718
|
+
_pendingImages.push(b64);
|
|
719
|
+
renderSubRows(rl.line || '');
|
|
720
|
+
} else if (_pendingImages.length > 0) {
|
|
721
|
+
_pendingImages.pop();
|
|
722
|
+
renderSubRows(rl.line || '');
|
|
723
|
+
} else {
|
|
724
|
+
flashSubRow(c.muted('✗ No image found in clipboard'));
|
|
710
725
|
}
|
|
726
|
+
} catch {
|
|
727
|
+
flashSubRow(c.muted('✗ Could not read clipboard'));
|
|
711
728
|
}
|
|
712
729
|
return;
|
|
713
730
|
}
|
|
@@ -748,12 +765,25 @@ async function startChat(opts = {}) {
|
|
|
748
765
|
process.exit(0);
|
|
749
766
|
});
|
|
750
767
|
|
|
751
|
-
const PROMPT
|
|
768
|
+
const PROMPT = () => c.brand(' ❯ ');
|
|
769
|
+
const ML_PROMPT = () => c.brand(' … ');
|
|
770
|
+
|
|
771
|
+
let _mlBuf = []; // multiline accumulation (\ continuation)
|
|
752
772
|
|
|
753
773
|
const loop = () => {
|
|
754
|
-
|
|
774
|
+
const isML = _mlBuf.length > 0;
|
|
775
|
+
rl.question(isML ? ML_PROMPT() : PROMPT(), async raw => {
|
|
755
776
|
clearSuggestions();
|
|
756
|
-
|
|
777
|
+
|
|
778
|
+
// Backslash continuation — collect lines until one doesn't end with \
|
|
779
|
+
if (raw.endsWith('\\')) {
|
|
780
|
+
_mlBuf.push(raw.slice(0, -1));
|
|
781
|
+
return loop();
|
|
782
|
+
}
|
|
783
|
+
_mlBuf.push(raw);
|
|
784
|
+
const input = _mlBuf.join('\n').trim();
|
|
785
|
+
_mlBuf = [];
|
|
786
|
+
|
|
757
787
|
if (!input) return loop();
|
|
758
788
|
|
|
759
789
|
// ── Slash commands ──────────────────────────────────────
|
|
@@ -985,16 +1015,18 @@ Keep it under 350 words. Write prior.md now.`;
|
|
|
985
1015
|
console.log('');
|
|
986
1016
|
|
|
987
1017
|
{
|
|
988
|
-
const
|
|
989
|
-
|
|
1018
|
+
const imagesForThisMsg = [..._pendingImages];
|
|
1019
|
+
_pendingImages = [];
|
|
990
1020
|
|
|
991
|
-
if (
|
|
992
|
-
|
|
1021
|
+
if (imagesForThisMsg.length > 0) {
|
|
1022
|
+
const label = imagesForThisMsg.length === 1 ? '1 image' : `${imagesForThisMsg.length} images`;
|
|
1023
|
+
console.log(c.brand(' ◈') + c.dim(` ${label} attached`));
|
|
993
1024
|
}
|
|
994
1025
|
|
|
995
|
-
let responseText
|
|
996
|
-
let _progressStarted
|
|
997
|
-
|
|
1026
|
+
let responseText = '';
|
|
1027
|
+
let _progressStarted = false;
|
|
1028
|
+
let _streamStarted = false; // true after first text_chunk
|
|
1029
|
+
const _thinkStart = Date.now();
|
|
998
1030
|
|
|
999
1031
|
spinStart('thinking…');
|
|
1000
1032
|
|
|
@@ -1017,7 +1049,7 @@ Keep it under 350 words. Write prior.md now.`;
|
|
|
1017
1049
|
model: currentModel,
|
|
1018
1050
|
cwd: process.cwd(),
|
|
1019
1051
|
projectContext,
|
|
1020
|
-
|
|
1052
|
+
images: imagesForThisMsg,
|
|
1021
1053
|
confirm,
|
|
1022
1054
|
signal: _currentAbortController.signal,
|
|
1023
1055
|
send: ev => {
|
|
@@ -1027,14 +1059,42 @@ Keep it under 350 words. Write prior.md now.`;
|
|
|
1027
1059
|
spinStart('thinking…');
|
|
1028
1060
|
break;
|
|
1029
1061
|
|
|
1062
|
+
case 'waiting':
|
|
1063
|
+
spinStart(`waiting for Ollama… (${ev.attempt}/${ev.max})`);
|
|
1064
|
+
break;
|
|
1065
|
+
|
|
1030
1066
|
case 'cancelled':
|
|
1031
1067
|
spinStop();
|
|
1032
|
-
process.stdout.write('\n');
|
|
1068
|
+
if (_streamStarted) process.stdout.write('\n');
|
|
1033
1069
|
console.log(c.muted(' ✗ Cancelled'));
|
|
1034
1070
|
break;
|
|
1035
1071
|
|
|
1072
|
+
// ── Streaming text events ──────────────────────
|
|
1073
|
+
case 'stream_start': {
|
|
1074
|
+
spinStop();
|
|
1075
|
+
const thinkTime = elapsed(Date.now() - _thinkStart);
|
|
1076
|
+
console.log(c.brand(' Prior ') + c.muted(`· ${timeNow()} · ${thinkTime}`));
|
|
1077
|
+
console.log('');
|
|
1078
|
+
process.stdout.write(' ');
|
|
1079
|
+
_streamStarted = true;
|
|
1080
|
+
break;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
case 'text_chunk':
|
|
1084
|
+
if (ev.content) {
|
|
1085
|
+
process.stdout.write(ev.content);
|
|
1086
|
+
responseText += ev.content;
|
|
1087
|
+
}
|
|
1088
|
+
break;
|
|
1089
|
+
|
|
1090
|
+
case 'stream_end':
|
|
1091
|
+
process.stdout.write('\n');
|
|
1092
|
+
_streamStarted = false;
|
|
1093
|
+
break;
|
|
1094
|
+
|
|
1036
1095
|
case 'tool_start':
|
|
1037
1096
|
spinStop();
|
|
1097
|
+
if (_streamStarted) { process.stdout.write('\n'); _streamStarted = false; }
|
|
1038
1098
|
_progressStarted = false;
|
|
1039
1099
|
renderToolStart(ev.name, ev.args);
|
|
1040
1100
|
if (!CONFIRM_TOOLS.has(ev.name)) spinStart('working…');
|
|
@@ -1057,7 +1117,7 @@ Keep it under 350 words. Write prior.md now.`;
|
|
|
1057
1117
|
|
|
1058
1118
|
case 'tool_done':
|
|
1059
1119
|
spinStop();
|
|
1060
|
-
renderToolDone(ev.name, ev.summary);
|
|
1120
|
+
renderToolDone(ev.name, ev.summary, ev.preview);
|
|
1061
1121
|
break;
|
|
1062
1122
|
|
|
1063
1123
|
case 'tool_skip':
|
|
@@ -1088,7 +1148,7 @@ Keep it under 350 words. Write prior.md now.`;
|
|
|
1088
1148
|
|
|
1089
1149
|
case 'error':
|
|
1090
1150
|
spinStop();
|
|
1091
|
-
process.stdout.write('\n');
|
|
1151
|
+
if (_streamStarted) process.stdout.write('\n');
|
|
1092
1152
|
console.error(c.err(` ✗ ${ev.message}`));
|
|
1093
1153
|
break;
|
|
1094
1154
|
}
|
package/lib/agent.js
CHANGED
|
@@ -8,21 +8,59 @@ const CLI_BASE = 'https://prior.ngrok.app/cli-backend';
|
|
|
8
8
|
const PRIOR_BASE = 'https://prior.ngrok.app';
|
|
9
9
|
const MAX_ITER = 14;
|
|
10
10
|
|
|
11
|
-
// ── Single inference call
|
|
11
|
+
// ── Single inference call — reads NDJSON stream from backend ──
|
|
12
12
|
|
|
13
|
-
async function infer(messages, model, token, { cwd, uncensored, projectContext,
|
|
13
|
+
async function infer(messages, model, token, { cwd, uncensored, projectContext, images } = {}, signal, onChunk) {
|
|
14
14
|
const res = await fetch(`${CLI_BASE}/api/infer`, {
|
|
15
15
|
method: 'POST',
|
|
16
16
|
headers: { 'Content-Type': 'application/json' },
|
|
17
|
-
body: JSON.stringify({ messages, model, token, cwd, uncensored, projectContext,
|
|
17
|
+
body: JSON.stringify({ messages, model, token, cwd, uncensored, projectContext, images }),
|
|
18
18
|
timeout: 120000,
|
|
19
19
|
signal,
|
|
20
20
|
});
|
|
21
|
+
|
|
21
22
|
if (!res.ok) {
|
|
22
23
|
const err = await res.json().catch(() => ({}));
|
|
23
24
|
throw new Error(err.error || `Server error: HTTP ${res.status}`);
|
|
24
25
|
}
|
|
25
|
-
|
|
26
|
+
|
|
27
|
+
let content = '';
|
|
28
|
+
let promptTokens = 0;
|
|
29
|
+
let completionTokens = 0;
|
|
30
|
+
|
|
31
|
+
await new Promise((resolve, reject) => {
|
|
32
|
+
let buf = '';
|
|
33
|
+
res.body.on('data', rawChunk => {
|
|
34
|
+
if (signal?.aborted) {
|
|
35
|
+
res.body.destroy();
|
|
36
|
+
return reject(Object.assign(new Error('AbortError'), { name: 'AbortError' }));
|
|
37
|
+
}
|
|
38
|
+
buf += rawChunk.toString();
|
|
39
|
+
const lines = buf.split('\n');
|
|
40
|
+
buf = lines.pop();
|
|
41
|
+
for (const line of lines) {
|
|
42
|
+
if (!line.trim()) continue;
|
|
43
|
+
try {
|
|
44
|
+
const data = JSON.parse(line);
|
|
45
|
+
if (data.type === 'chunk') {
|
|
46
|
+
if (onChunk) onChunk(data.content);
|
|
47
|
+
} else if (data.type === 'done') {
|
|
48
|
+
content = data.content || '';
|
|
49
|
+
promptTokens = data.promptTokens || 0;
|
|
50
|
+
completionTokens = data.completionTokens || 0;
|
|
51
|
+
} else if (data.type === 'waiting') {
|
|
52
|
+
if (onChunk) onChunk(null, { type: 'waiting', attempt: data.attempt, max: data.max });
|
|
53
|
+
} else if (data.type === 'error') {
|
|
54
|
+
reject(new Error(data.message));
|
|
55
|
+
}
|
|
56
|
+
} catch { /* skip malformed line */ }
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
res.body.on('end', resolve);
|
|
60
|
+
res.body.on('error', reject);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return { content, promptTokens, completionTokens };
|
|
26
64
|
}
|
|
27
65
|
|
|
28
66
|
// ── Token usage tracking ──────────────────────────────────────
|
|
@@ -200,41 +238,83 @@ function stripToolTags(text) {
|
|
|
200
238
|
|
|
201
239
|
const CONFIRM_TOOLS = new Set(['run_command', 'file_delete', 'file_write']);
|
|
202
240
|
|
|
203
|
-
async function runAgent({ messages, model, uncensored, cwd, projectContext,
|
|
204
|
-
const token
|
|
241
|
+
async function runAgent({ messages, model, uncensored, cwd, projectContext, images, send, confirm, signal }) {
|
|
242
|
+
const token = getToken();
|
|
205
243
|
const history = [...messages];
|
|
206
244
|
|
|
207
245
|
let totalPromptTokens = 0;
|
|
208
246
|
let totalCompletionTokens = 0;
|
|
209
|
-
let
|
|
247
|
+
let pendingImages = (images && images.length) ? images : null;
|
|
210
248
|
|
|
211
249
|
for (let iter = 0; iter < MAX_ITER; iter++) {
|
|
212
250
|
|
|
213
|
-
if (signal?.aborted) {
|
|
214
|
-
send({ type: 'cancelled' });
|
|
215
|
-
send({ type: 'done' });
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
251
|
+
if (signal?.aborted) { send({ type: 'cancelled' }); send({ type: 'done' }); return; }
|
|
218
252
|
|
|
219
253
|
send({ type: 'thinking' });
|
|
220
254
|
|
|
255
|
+
// ── Per-iteration streaming state ─────────────────────────
|
|
256
|
+
// After </think> we buffer LOOK_SIZE chars to detect tool calls before
|
|
257
|
+
// deciding whether to stream text live or stay in buffered mode.
|
|
258
|
+
let thinkBuf = ''; // chunks buffered while inside <think>…</think>
|
|
259
|
+
let thinkDone = false;
|
|
260
|
+
let lookBuf = ''; // first N chars of actual response (after think)
|
|
261
|
+
let streaming = false; // true once we've committed to live-streaming text
|
|
262
|
+
const LOOK_SIZE = 60;
|
|
263
|
+
|
|
264
|
+
function tryStartStreaming() {
|
|
265
|
+
if (streaming) return;
|
|
266
|
+
const trimmed = lookBuf.replace(/^[\s\n]+/, '');
|
|
267
|
+
// If the response starts with a tool tag, keep buffered (no live text)
|
|
268
|
+
if (/^<(?:tool|write|append|docx)[\s>{"[]/.test(trimmed)) return;
|
|
269
|
+
streaming = true;
|
|
270
|
+
send({ type: 'stream_start' });
|
|
271
|
+
if (trimmed) send({ type: 'text_chunk', content: trimmed });
|
|
272
|
+
lookBuf = '';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const onChunk = (raw, meta) => {
|
|
276
|
+
if (meta?.type === 'waiting') { send({ type: 'waiting', attempt: meta.attempt, max: meta.max }); return; }
|
|
277
|
+
if (!raw) return;
|
|
278
|
+
|
|
279
|
+
if (!thinkDone) {
|
|
280
|
+
thinkBuf += raw;
|
|
281
|
+
const idx = thinkBuf.indexOf('</think>');
|
|
282
|
+
if (idx !== -1) {
|
|
283
|
+
thinkDone = true;
|
|
284
|
+
lookBuf = thinkBuf.slice(idx + 8).replace(/^[\s\n]+/, '');
|
|
285
|
+
thinkBuf = '';
|
|
286
|
+
if (lookBuf.length >= LOOK_SIZE) tryStartStreaming();
|
|
287
|
+
}
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (!streaming) {
|
|
292
|
+
lookBuf += raw;
|
|
293
|
+
if (lookBuf.length >= LOOK_SIZE) tryStartStreaming();
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
send({ type: 'text_chunk', content: raw });
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const iterImages = pendingImages;
|
|
301
|
+
pendingImages = null;
|
|
302
|
+
|
|
221
303
|
let result;
|
|
222
|
-
const iterImage = pendingImage;
|
|
223
|
-
pendingImage = null; // clear after first use
|
|
224
304
|
try {
|
|
225
|
-
result = await infer(history, model || 'qwen3.5:4b', token, { cwd, uncensored, projectContext,
|
|
305
|
+
result = await infer(history, model || 'qwen3.5:4b', token, { cwd, uncensored, projectContext, images: iterImages }, signal, onChunk);
|
|
226
306
|
} catch (err) {
|
|
227
307
|
await trackTokenUsage(token, totalPromptTokens, totalCompletionTokens);
|
|
228
|
-
if (err.name === 'AbortError' || signal?.aborted) {
|
|
229
|
-
send({ type: 'cancelled' });
|
|
230
|
-
send({ type: 'done' });
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
308
|
+
if (err.name === 'AbortError' || signal?.aborted) { send({ type: 'cancelled' }); send({ type: 'done' }); return; }
|
|
233
309
|
send({ type: 'error', message: err.message });
|
|
234
310
|
send({ type: 'done' });
|
|
235
311
|
return;
|
|
236
312
|
}
|
|
237
313
|
|
|
314
|
+
// Flush look-ahead if stream ended before LOOK_SIZE was reached
|
|
315
|
+
if (thinkDone && !streaming && lookBuf) tryStartStreaming();
|
|
316
|
+
if (streaming) send({ type: 'stream_end' });
|
|
317
|
+
|
|
238
318
|
totalPromptTokens += result.promptTokens || 0;
|
|
239
319
|
totalCompletionTokens += result.completionTokens || 0;
|
|
240
320
|
|
|
@@ -252,20 +332,21 @@ async function runAgent({ messages, model, uncensored, cwd, projectContext, imag
|
|
|
252
332
|
if (calls.length === 0) {
|
|
253
333
|
const finalText = stripToolTags(cleaned);
|
|
254
334
|
if (!finalText && iter < MAX_ITER - 1) {
|
|
255
|
-
// Model returned blank (all think tags, no actual output) — nudge once
|
|
256
335
|
history.push({ role: 'assistant', content: raw });
|
|
257
336
|
history.push({ role: 'user', content: '(Your response was empty. Please write your reply.)' });
|
|
258
337
|
continue;
|
|
259
338
|
}
|
|
260
339
|
await trackTokenUsage(token, totalPromptTokens, totalCompletionTokens);
|
|
261
|
-
send({ type: 'text', content: finalText });
|
|
340
|
+
if (!streaming) send({ type: 'text', content: finalText }); // already shown if streaming
|
|
262
341
|
send({ type: 'done' });
|
|
263
342
|
return;
|
|
264
343
|
}
|
|
265
344
|
|
|
266
345
|
// ── Text before first tool call ───────────────────────────
|
|
267
|
-
|
|
268
|
-
|
|
346
|
+
if (!streaming) {
|
|
347
|
+
const textBefore = stripToolTags(cleaned.slice(0, calls[0].offset)).trim();
|
|
348
|
+
if (textBefore) send({ type: 'text', content: textBefore });
|
|
349
|
+
}
|
|
269
350
|
|
|
270
351
|
history.push({ role: 'assistant', content: raw });
|
|
271
352
|
|
|
@@ -274,7 +355,6 @@ async function runAgent({ messages, model, uncensored, cwd, projectContext, imag
|
|
|
274
355
|
for (const call of calls) {
|
|
275
356
|
send({ type: 'tool_start', name: call.name, args: call.args });
|
|
276
357
|
|
|
277
|
-
// Confirmation gate for destructive / side-effect tools
|
|
278
358
|
if (confirm && CONFIRM_TOOLS.has(call.name)) {
|
|
279
359
|
const approved = await confirm({ name: call.name, args: call.args });
|
|
280
360
|
if (!approved) {
|
|
@@ -286,7 +366,8 @@ async function runAgent({ messages, model, uncensored, cwd, projectContext, imag
|
|
|
286
366
|
|
|
287
367
|
try {
|
|
288
368
|
const toolResult = await executeTool(call.name, call.args, { cwd, token, send });
|
|
289
|
-
|
|
369
|
+
// Pass output snippet so the CLI can show a rich preview
|
|
370
|
+
send({ type: 'tool_done', name: call.name, summary: toolResult.summary, preview: toolResult.output });
|
|
290
371
|
resultParts.push(`<tool_result name="${call.name}">\n${toolResult.output}\n</tool_result>`);
|
|
291
372
|
} catch (err) {
|
|
292
373
|
send({ type: 'tool_error', name: call.name, error: err.message });
|