@semalt-ai/code 1.8.4 → 1.8.5
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/.claude/settings.local.json +3 -1
- package/CLAUDE.md +4 -1
- package/TECHNICAL_DEBT.md +66 -0
- package/index.js +9 -2
- package/lib/agent.js +234 -87
- package/lib/api.js +95 -6
- package/lib/args.js +22 -0
- package/lib/commands.js +168 -18
- package/lib/config.js +13 -0
- package/lib/debug.js +106 -0
- package/lib/proc.js +96 -0
- package/lib/prompts.js +4 -3
- package/lib/tool_specs.js +14 -7
- package/lib/tools.js +287 -113
- package/lib/ui/chat-history.js +19 -1
- package/lib/ui/format.js +79 -5
- package/lib/ui/terminal.js +10 -4
- package/lib/ui/writer.js +7 -9
- package/package.json +1 -1
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
"Bash(python3 *)",
|
|
16
16
|
"Read(//tmp/**)",
|
|
17
17
|
"Bash(sed -i \"s/addMessage\\('>>> AI MSG 2'.*$/addMessage\\('>>> AI MSG 2', ['response body 2a', 'response body 2b']\\);\\\\nfor \\(let k = 3; k <= 8; k++\\) { addMessage\\('>>> USER MSG ' + k, ['line body ' + k]\\); addMessage\\('>>> AI MSG ' + k, ['response body ' + k + 'a', 'response body ' + k + 'b']\\); }/\" scroll-capture.js)",
|
|
18
|
-
"Bash(echo \"exit=$?\")"
|
|
18
|
+
"Bash(echo \"exit=$?\")",
|
|
19
|
+
"Bash(echo \"---grep done, exit=$?---\")",
|
|
20
|
+
"Bash(grep *)"
|
|
19
21
|
]
|
|
20
22
|
}
|
|
21
23
|
}
|
package/CLAUDE.md
CHANGED
|
@@ -87,7 +87,10 @@ semalt-code config [set <key> <val>] # show or update config keys
|
|
|
87
87
|
--dashboard-url <url> dashboard base URL (overrides config)
|
|
88
88
|
--default-model <name> set default model in config
|
|
89
89
|
--show-think display model reasoning (thinking) content
|
|
90
|
-
--debug
|
|
90
|
+
--debug inline debug: per-iteration debug block in chat history (TUI-safe)
|
|
91
|
+
--debug-file <path> extended debug: per-iteration block + raw SSE chunks
|
|
92
|
+
+ request body dumps written to <path>, nothing to stdout.
|
|
93
|
+
Mutually exclusive with --debug.
|
|
91
94
|
--allow-fs auto-approve all filesystem operations
|
|
92
95
|
--allow-exec auto-approve shell command execution
|
|
93
96
|
--allow-net auto-approve network operations
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
## Activity region in-place update breaks when a modal is open
|
|
2
|
+
|
|
3
|
+
When a modal occupies screen lines below an active activity bubble, the
|
|
4
|
+
activity region's redraw mechanism appears to fall back to scrollback
|
|
5
|
+
append per tick instead of in-place rewrite. Surfaced via the `ask_user`
|
|
6
|
+
ticking-timer bug; mitigated by making `ask_user` a static bubble.
|
|
7
|
+
|
|
8
|
+
Latent: any future long-running tool that opens a modal concurrently
|
|
9
|
+
(none today) will reproduce the fragmentation. Fix likely involves
|
|
10
|
+
making the activity region modal-aware in `lib/ui/writer.js` — when a
|
|
11
|
+
modal region is active, route activity updates through a path that
|
|
12
|
+
clears modal, redraws activity, redraws modal — or reserves activity
|
|
13
|
+
above the modal in a way that survives modal lifecycle.
|
|
14
|
+
|
|
15
|
+
Not blocking. Revisit if a second use-case appears.
|
|
16
|
+
|
|
17
|
+
## `cmdShell` and `chatStream` write to stdout bypassing the writer
|
|
18
|
+
|
|
19
|
+
Several call sites currently emit directly to `process.stdout.write`
|
|
20
|
+
without going through `lib/ui/writer.js`:
|
|
21
|
+
|
|
22
|
+
- `lib/commands.js` (`cmdShell`)
|
|
23
|
+
- `lib/api.js` (streaming output path)
|
|
24
|
+
|
|
25
|
+
These were flagged during the Phase 2 writer audit and annotated as
|
|
26
|
+
`// audit: allowed` because they need to interleave with synchronous
|
|
27
|
+
writes from `StreamRenderer`. Routing them through the writer today
|
|
28
|
+
would require buffering or sequencing changes that don't compose with
|
|
29
|
+
how `StreamRenderer` flushes content per chunk.
|
|
30
|
+
|
|
31
|
+
Resolves when: `StreamRenderer` itself is migrated to write through
|
|
32
|
+
the writer. After that, the bypass annotations can be removed and
|
|
33
|
+
these call sites become normal `writer.scrollback(...)` calls.
|
|
34
|
+
|
|
35
|
+
Not blocking. The audit annotation makes the bypass intentional and
|
|
36
|
+
greppable. Revisit when `StreamRenderer` migration is on the table.
|
|
37
|
+
|
|
38
|
+
## Tool result storage: single `content` field used for both model and UI
|
|
39
|
+
|
|
40
|
+
Storage (PHP backend, MySQL `messages` table) holds one `content` field
|
|
41
|
+
per tool result. The full payload is required for the model on
|
|
42
|
+
subsequent turns, but the UI needs a compact summary (e.g. `net · GET
|
|
43
|
+
https://... · 200 · 256 KB`).
|
|
44
|
+
|
|
45
|
+
Today this is handled UI-side: `summarizeToolResult` in
|
|
46
|
+
`lib/ui/format.js` runs read-side heuristics on the raw `content` every
|
|
47
|
+
time `/history` renders. Heuristics cover HTTP, exec, file ops, with
|
|
48
|
+
a fallback for unknown shapes. They work in practice but are a
|
|
49
|
+
compromise — any tool whose output format drifts will fall through to
|
|
50
|
+
the generic fallback until the heuristic is updated.
|
|
51
|
+
|
|
52
|
+
Full fix: storage holds both `content` (full, model-bound) and
|
|
53
|
+
`display` (pre-rendered summary, UI-bound). Summary is generated
|
|
54
|
+
write-side at tool execution time, when the live activity bubble
|
|
55
|
+
already produces the right string — that string just needs to be
|
|
56
|
+
captured and persisted alongside the full content.
|
|
57
|
+
|
|
58
|
+
Resolves when: backend schema migration for native function calling
|
|
59
|
+
lands (Phase 2.2 of the native-tools plan, which already touches the
|
|
60
|
+
`messages` table). Adding a `display` column in the same migration is
|
|
61
|
+
cheap; doing it as a separate migration later is not. When this lands,
|
|
62
|
+
`summarizeToolResult` becomes unnecessary for new tool results; it
|
|
63
|
+
stays only as a fallback for legacy rows lacking `display`.
|
|
64
|
+
|
|
65
|
+
Not blocking — current heuristics cover all 33 tools' output shapes.
|
|
66
|
+
Track until Phase 2.2 lands.
|
package/index.js
CHANGED
|
@@ -49,7 +49,7 @@ if (_argv.includes('--allow-all')) {
|
|
|
49
49
|
const _readonly = _argv.includes('--readonly');
|
|
50
50
|
|
|
51
51
|
const permissionManager = createPermissionManager(ui, { allowedTiers: _allowedTiers, readonly: _readonly });
|
|
52
|
-
const { agentExecShell, agentExecFile } = createToolExecutor(permissionManager, ui, getConfig);
|
|
52
|
+
const { agentExecShell, agentExecFile, describePermission } = createToolExecutor(permissionManager, ui, getConfig);
|
|
53
53
|
const apiClient = createApiClient({
|
|
54
54
|
getConfig,
|
|
55
55
|
saveConfig: (nextConfig) => {
|
|
@@ -66,6 +66,8 @@ const { runAgentLoop } = createAgentRunner({
|
|
|
66
66
|
}),
|
|
67
67
|
agentExecShell,
|
|
68
68
|
agentExecFile,
|
|
69
|
+
describePermission,
|
|
70
|
+
permissionManager,
|
|
69
71
|
ui,
|
|
70
72
|
getConfig,
|
|
71
73
|
});
|
|
@@ -119,7 +121,12 @@ Options:
|
|
|
119
121
|
--dashboard-url <url> Dashboard URL (init)
|
|
120
122
|
--default-model <name> Default model (init)
|
|
121
123
|
--show-think Display model reasoning (thinking) content
|
|
122
|
-
--debug
|
|
124
|
+
--debug Inline debug output: per-iteration debug block in the
|
|
125
|
+
chat history. TUI-safe; no per-chunk noise.
|
|
126
|
+
--debug-file <path> Extended debug to file: per-iteration block PLUS raw
|
|
127
|
+
SSE chunks, request body dumps, accumulator state,
|
|
128
|
+
and other high-volume traces. Nothing prints to stdout
|
|
129
|
+
— the TUI stays clean. Mutually exclusive with --debug.
|
|
123
130
|
--allow-fs Auto-approve all filesystem operations
|
|
124
131
|
--allow-exec Auto-approve shell command execution
|
|
125
132
|
--allow-net Auto-approve network operations
|
package/lib/agent.js
CHANGED
|
@@ -3,13 +3,16 @@
|
|
|
3
3
|
const { logToolCall } = require('./audit');
|
|
4
4
|
const { Metrics } = require('./metrics');
|
|
5
5
|
const { getSystemPrompt } = require('./prompts');
|
|
6
|
+
const { isNativeToolsActive } = require('./config');
|
|
6
7
|
const { TAG_REGISTRY } = require('./constants');
|
|
7
8
|
const { mapInvokeToCall } = require('./tools');
|
|
9
|
+
const { TOOL_SPECS } = require('./tool_specs');
|
|
8
10
|
const { UI_THEME } = require('./ui/theme');
|
|
9
11
|
const { RST } = require('./ui/ansi');
|
|
10
12
|
const { getCols: _getCols, repeatToWidth } = require('./ui/utils');
|
|
11
13
|
const writer = require('./ui/writer');
|
|
12
14
|
const messages = require('./ui/messages');
|
|
15
|
+
const dbg = require('./debug');
|
|
13
16
|
|
|
14
17
|
class StreamParser {
|
|
15
18
|
constructor(onToken, onTagOpen, onTagContent, onTagClose) {
|
|
@@ -180,7 +183,8 @@ function abortableSleep(ms, signal) {
|
|
|
180
183
|
});
|
|
181
184
|
}
|
|
182
185
|
|
|
183
|
-
function detectFormat(reply, toolCalls) {
|
|
186
|
+
function detectFormat(reply, toolCalls, nativeToolCalls) {
|
|
187
|
+
if (nativeToolCalls && nativeToolCalls.length > 0) return 'native_tool_calls';
|
|
184
188
|
if (!reply || !reply.trim()) return 'empty';
|
|
185
189
|
if (/<(minimax:tool_call|qwen:tool_call|tool_call|function_call)\b/i.test(reply)) return 'tool_call';
|
|
186
190
|
if (toolCalls && toolCalls.length > 0) return 'command';
|
|
@@ -222,6 +226,26 @@ function previewCommand(call) {
|
|
|
222
226
|
return trimmed ? `<${tag}> ${trimmed}` : `<${tag}>`;
|
|
223
227
|
}
|
|
224
228
|
|
|
229
|
+
// Classify why mapInvokeToCall returned null for a native tool_call so the
|
|
230
|
+
// debug block (and the corrective retry hint) can surface the specific cause
|
|
231
|
+
// instead of a generic "unknown name or invalid args". Source of truth is
|
|
232
|
+
// TOOL_SPECS — its `required` array tells us which positional args the
|
|
233
|
+
// native API advertised, and `wrapper:true` flags parser envelopes that
|
|
234
|
+
// must never appear as a model-emitted tool name.
|
|
235
|
+
function describeNativeRejection(toolName, params) {
|
|
236
|
+
const lowerName = (toolName || '').toLowerCase();
|
|
237
|
+
const spec = TOOL_SPECS[lowerName];
|
|
238
|
+
if (!spec || spec.wrapper) {
|
|
239
|
+
return 'unknown name (not in TOOL_SPECS / not supported by mapInvokeToCall)';
|
|
240
|
+
}
|
|
241
|
+
const required = (spec.parameters && spec.parameters.required) || [];
|
|
242
|
+
const missing = required.filter((r) => params[r] === undefined || params[r] === null);
|
|
243
|
+
if (missing.length > 0) {
|
|
244
|
+
return `missing required arg: ${missing.join(', ')}`;
|
|
245
|
+
}
|
|
246
|
+
return 'mapInvokeToCall returned null without specific reason';
|
|
247
|
+
}
|
|
248
|
+
|
|
225
249
|
function formatDebugBlock(sections) {
|
|
226
250
|
// The debug block is rendered as a tool-output message in the TUI. Chat
|
|
227
251
|
// history indents output by 5 cols; account for that so the frame still
|
|
@@ -418,7 +442,7 @@ function _attrsFromCall(call) {
|
|
|
418
442
|
}
|
|
419
443
|
}
|
|
420
444
|
|
|
421
|
-
function createAgentRunner({ chatStream, extractToolCalls, agentExecShell, agentExecFile, ui, getConfig }) {
|
|
445
|
+
function createAgentRunner({ chatStream, extractToolCalls, agentExecShell, agentExecFile, describePermission, permissionManager, ui, getConfig }) {
|
|
422
446
|
const { BOLD, FG_DARK, FG_GRAY, FG_TEAL, FG_YELLOW, RST, THEME, getCols } = ui;
|
|
423
447
|
|
|
424
448
|
function formatFileResult(call, result) {
|
|
@@ -542,8 +566,7 @@ function createAgentRunner({ chatStream, extractToolCalls, agentExecShell, agent
|
|
|
542
566
|
}
|
|
543
567
|
case 'http_get': {
|
|
544
568
|
const url = attrs.url || content;
|
|
545
|
-
|
|
546
|
-
return formatFileResult(['http_get', url, raw], await agentExecFile('http_get', url, raw));
|
|
569
|
+
return formatFileResult(['http_get', url], await agentExecFile('http_get', url));
|
|
547
570
|
}
|
|
548
571
|
case 'ask_user': {
|
|
549
572
|
const q = attrs.question || content;
|
|
@@ -598,22 +621,23 @@ function createAgentRunner({ chatStream, extractToolCalls, agentExecShell, agent
|
|
|
598
621
|
const metrics = new Metrics(tokenLimit);
|
|
599
622
|
const mode = overrideMode || 'system_role';
|
|
600
623
|
|
|
601
|
-
// Route debug blocks
|
|
602
|
-
//
|
|
603
|
-
//
|
|
624
|
+
// Route debug blocks based on debug mode.
|
|
625
|
+
// file mode — write to the debug file. Never touch the TUI.
|
|
626
|
+
// simple mode — UI callback when present (chat-bubble in interactive
|
|
627
|
+
// TUI), fall back to stderr for one-shot/non-TTY flows.
|
|
628
|
+
// off mode — discard. (debug=true can also come from in-chat /debug
|
|
629
|
+
// toggle with no global mode active.)
|
|
604
630
|
const emitDebug = (block) => {
|
|
631
|
+
if (dbg.isFile()) {
|
|
632
|
+
dbg.log(block);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
605
635
|
if (typeof cb.onDebug === 'function') cb.onDebug(block);
|
|
606
636
|
// audit: allowed — stderr debug under --debug flag (no UI hosting available).
|
|
607
637
|
else process.stderr.write('\n' + block + '\n');
|
|
608
638
|
};
|
|
609
639
|
|
|
610
|
-
|
|
611
|
-
// Fallback to true if no matching profile — mirrors config-normalization default.
|
|
612
|
-
const _cfg = typeof getConfig === 'function' ? getConfig() : {};
|
|
613
|
-
const _profile = Array.isArray(_cfg.models)
|
|
614
|
-
? _cfg.models.find((p) => p && p.api_base === _cfg.api_base && p.model === model)
|
|
615
|
-
: null;
|
|
616
|
-
const nativeTools = _profile && _profile.native_tools === false ? false : true;
|
|
640
|
+
const nativeTools = isNativeToolsActive(model);
|
|
617
641
|
|
|
618
642
|
const activeSystemPrompt = overrideSystemPrompt !== null ? overrideSystemPrompt : getSystemPrompt(nativeTools);
|
|
619
643
|
|
|
@@ -854,20 +878,35 @@ function createAgentRunner({ chatStream, extractToolCalls, agentExecShell, agent
|
|
|
854
878
|
const nativeToolCalls = Array.isArray(result?.toolCalls) ? result.toolCalls : [];
|
|
855
879
|
let toolCalls;
|
|
856
880
|
let nativeToolCallIds = [];
|
|
881
|
+
// Per-call rejection records for native tool_calls that could not be
|
|
882
|
+
// converted to executable form (parse error or unknown name / missing
|
|
883
|
+
// required arg). Used downstream to (a) keep the assistant's tool_calls
|
|
884
|
+
// ↔ tool-result map consistent, and (b) feed a corrective hint back to
|
|
885
|
+
// the model so it retries instead of stalling.
|
|
886
|
+
const nativeRejections = [];
|
|
857
887
|
if (nativeToolCalls.length > 0) {
|
|
858
888
|
toolCalls = [];
|
|
859
889
|
for (const tc of nativeToolCalls) {
|
|
890
|
+
const fnName = tc.function?.name || '(unknown)';
|
|
891
|
+
const argsRaw = tc.function?.arguments || '';
|
|
892
|
+
const argsPreview = argsRaw.length > 200 ? argsRaw.slice(0, 200) + '…' : argsRaw;
|
|
860
893
|
let args;
|
|
861
894
|
try {
|
|
862
|
-
args =
|
|
895
|
+
args = argsRaw ? JSON.parse(argsRaw) : {};
|
|
863
896
|
} catch (err) {
|
|
864
|
-
|
|
897
|
+
const reason = `JSON parse failed: ${err.message}`;
|
|
898
|
+
if (cb.onError) cb.onError({ message: `${fnName}: ${reason} Args: ${argsPreview}`, isWarning: true });
|
|
899
|
+
nativeRejections.push({ id: tc.id, name: fnName, argsPreview, reason });
|
|
865
900
|
continue;
|
|
866
901
|
}
|
|
867
|
-
const call = mapInvokeToCall(
|
|
902
|
+
const call = mapInvokeToCall(fnName, args);
|
|
868
903
|
if (call) {
|
|
869
904
|
toolCalls.push(call);
|
|
870
905
|
nativeToolCallIds.push(tc.id);
|
|
906
|
+
} else {
|
|
907
|
+
const reason = describeNativeRejection(fnName, args);
|
|
908
|
+
if (cb.onError) cb.onError({ message: `${fnName}: ${reason} Args: ${argsPreview}`, isWarning: true });
|
|
909
|
+
nativeRejections.push({ id: tc.id, name: fnName, argsPreview, reason });
|
|
871
910
|
}
|
|
872
911
|
}
|
|
873
912
|
} else {
|
|
@@ -895,17 +934,27 @@ function createAgentRunner({ chatStream, extractToolCalls, agentExecShell, agent
|
|
|
895
934
|
const visibleTokens = Math.max(completionTokens - thinkingTokens, 0);
|
|
896
935
|
const contextLimit = tokenLimit || null;
|
|
897
936
|
const ctxPct = contextLimit ? Math.round((promptTokens / contextLimit) * 100) : null;
|
|
898
|
-
const detected = detectFormat(reply, toolCalls);
|
|
937
|
+
const detected = detectFormat(reply, toolCalls, nativeToolCalls);
|
|
899
938
|
const firstCmd = toolCalls.length > 0 ? previewCommand(toolCalls[0]) : previewCommand(null);
|
|
900
939
|
const toolTags = Object.entries(TAG_REGISTRY)
|
|
901
940
|
.filter(([, e]) => e.type === 'tool')
|
|
902
941
|
.map(([t]) => t);
|
|
942
|
+
const callableSpecCount = Object.values(TOOL_SPECS).filter((s) => !s.wrapper).length;
|
|
903
943
|
|
|
904
944
|
const warnings = [];
|
|
905
945
|
if (result.finish_reason === 'length') warnings.push('finish_reason=length → response truncated, increase max_tokens');
|
|
906
946
|
if (detected === 'tool_call' && toolCalls.length === 0) {
|
|
907
947
|
warnings.push('commands_found=0 → agent emitted no command, client will stall');
|
|
908
948
|
}
|
|
949
|
+
if (detected === 'native_tool_calls' && toolCalls.length === 0) {
|
|
950
|
+
const lines = [`commands_found=0 → all ${nativeToolCalls.length} native tool_call(s) rejected:`];
|
|
951
|
+
for (const r of nativeRejections) {
|
|
952
|
+
lines.push(` • name="${r.name}"`);
|
|
953
|
+
lines.push(` args=${r.argsPreview || '(empty)'}`);
|
|
954
|
+
lines.push(` reason=${r.reason}`);
|
|
955
|
+
}
|
|
956
|
+
warnings.push(lines.join('\n'));
|
|
957
|
+
}
|
|
909
958
|
if (ctxPct !== null && ctxPct > 80) warnings.push(`context_used=${ctxPct}% → approaching context limit`);
|
|
910
959
|
|
|
911
960
|
const block = formatDebugBlock({
|
|
@@ -931,7 +980,9 @@ function createAgentRunner({ chatStream, extractToolCalls, agentExecShell, agent
|
|
|
931
980
|
['temperature:', result.request?.temperature ?? '(default)'],
|
|
932
981
|
['stop_sequences:', JSON.stringify(result.request?.stop || [])],
|
|
933
982
|
['reasoning_effort:', '(n/a)'],
|
|
934
|
-
['tools_enabled:',
|
|
983
|
+
['tools_enabled:', nativeTools
|
|
984
|
+
? `${callableSpecCount} functions (via tools API)`
|
|
985
|
+
: `${toolTags.length} XML tags (via system prompt)`],
|
|
935
986
|
]],
|
|
936
987
|
['RESPONSE', [
|
|
937
988
|
['finish_reason:', result.finish_reason || '(unknown)'],
|
|
@@ -981,7 +1032,13 @@ function createAgentRunner({ chatStream, extractToolCalls, agentExecShell, agent
|
|
|
981
1032
|
}
|
|
982
1033
|
|
|
983
1034
|
const assistantMsg = { role: 'assistant', content: cleanedReply };
|
|
984
|
-
|
|
1035
|
+
// Only attach tool_calls for the calls we actually accepted. Attaching
|
|
1036
|
+
// rejected calls here would leave them without matching `tool` results
|
|
1037
|
+
// on the next turn — strict providers reject the resulting history.
|
|
1038
|
+
if (isNativeCall && nativeToolCallIds.length > 0) {
|
|
1039
|
+
const acceptedSet = new Set(nativeToolCallIds);
|
|
1040
|
+
assistantMsg.tool_calls = nativeToolCalls.filter((tc) => acceptedSet.has(tc.id));
|
|
1041
|
+
}
|
|
985
1042
|
messages.push(assistantMsg);
|
|
986
1043
|
// When showThink is off and the turn has tool calls, suppress the text bubble —
|
|
987
1044
|
// pre-tool reasoning is noise, tool result bubbles already convey what happened.
|
|
@@ -989,6 +1046,29 @@ function createAgentRunner({ chatStream, extractToolCalls, agentExecShell, agent
|
|
|
989
1046
|
if (cb.onAssistantMessage) cb.onAssistantMessage(displayReply);
|
|
990
1047
|
|
|
991
1048
|
if (toolCalls.length === 0) {
|
|
1049
|
+
// Native mode: tool_calls came in but none could be converted (parse
|
|
1050
|
+
// error or unknown name / missing required arg). Push a corrective
|
|
1051
|
+
// user hint so the model retries instead of stalling. Without this
|
|
1052
|
+
// the loop would break silently — that's the bug the migration set
|
|
1053
|
+
// out to fix.
|
|
1054
|
+
if (isNativeCall && nativeRejections.length > 0) {
|
|
1055
|
+
const summary = nativeRejections
|
|
1056
|
+
.map((r) => `- ${r.name}: ${r.reason}`)
|
|
1057
|
+
.join('\n');
|
|
1058
|
+
if (cb.onError) {
|
|
1059
|
+
const names = nativeRejections.map((r) => r.name).join(', ');
|
|
1060
|
+
cb.onError({
|
|
1061
|
+
message: `Native tool_call(s) rejected: ${names}. Asking the model to retry with a valid call.`,
|
|
1062
|
+
isWarning: true,
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
messages.push({
|
|
1066
|
+
role: 'user',
|
|
1067
|
+
content: `Your last response contained tool_calls that could not be executed:\n\n${summary}\n\nRetry with a valid tool name and complete required arguments per the tools schema.`,
|
|
1068
|
+
});
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
992
1072
|
// Detect malformed known-tag syntax (e.g. <create_file> with no path
|
|
993
1073
|
// attribute, usually paired with nonsense like <attrs: path=...> inside
|
|
994
1074
|
// the body). Push a corrective feedback message and keep looping so
|
|
@@ -1027,36 +1107,125 @@ function createAgentRunner({ chatStream, extractToolCalls, agentExecShell, agent
|
|
|
1027
1107
|
// never reused even if the agent runs the same tag twice.
|
|
1028
1108
|
let invocationCounter = 0;
|
|
1029
1109
|
|
|
1030
|
-
for
|
|
1031
|
-
|
|
1110
|
+
// Re-arm the abort watcher for the tool-execution phase. The API-call
|
|
1111
|
+
// finally cleared the previous one, so without this a Ctrl+C while a
|
|
1112
|
+
// long shell command is running would never reach the AbortSignal we
|
|
1113
|
+
// now thread into agentExecShell — the child would keep running and
|
|
1114
|
+
// the UI would show "Interrupted" without actually killing anything.
|
|
1115
|
+
const toolAbortWatcher = setInterval(() => {
|
|
1116
|
+
if (isAborted() && !controller.signal.aborted) controller.abort();
|
|
1117
|
+
}, 50);
|
|
1032
1118
|
|
|
1033
|
-
|
|
1034
|
-
const
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1119
|
+
try {
|
|
1120
|
+
for (const call of toolCalls) {
|
|
1121
|
+
if (isAborted()) { aborted = true; break; }
|
|
1122
|
+
|
|
1123
|
+
const tag = call[0] || 'unknown';
|
|
1124
|
+
const arg = call[1] || '';
|
|
1125
|
+
const attrs = _attrsFromCall(call);
|
|
1126
|
+
|
|
1127
|
+
// Permission gate, lifted out of the executors. Asking before
|
|
1128
|
+
// onToolStart fires means the activity bubble (and its 1Hz
|
|
1129
|
+
// ticker) doesn't pre-date grant — and on denial no bubble
|
|
1130
|
+
// appears at all. The picker's own onCloseModal scrollback
|
|
1131
|
+
// line ("✗ <description>") is the visual record of the denial.
|
|
1132
|
+
let permDesc = null;
|
|
1133
|
+
try {
|
|
1134
|
+
permDesc = describePermission ? await describePermission(call) : null;
|
|
1135
|
+
} catch (err) {
|
|
1136
|
+
if (cb.onError) cb.onError({ message: `describePermission(${tag}): ${err.message}`, isWarning: true });
|
|
1137
|
+
}
|
|
1138
|
+
if (permDesc) {
|
|
1139
|
+
if (cb.onPermissionAsk) cb.onPermissionAsk(tag, arg);
|
|
1140
|
+
let approved = true;
|
|
1141
|
+
try {
|
|
1142
|
+
approved = await permissionManager.askPermission(permDesc.actionType, permDesc.description, permDesc.tag);
|
|
1143
|
+
} catch (err) {
|
|
1144
|
+
if (cb.onError) cb.onError({ message: `askPermission(${tag}): ${err.message}`, isWarning: true });
|
|
1145
|
+
approved = false;
|
|
1146
|
+
}
|
|
1147
|
+
if (!approved) {
|
|
1148
|
+
const resultStr = (tag === 'shell' || tag === 'exec')
|
|
1149
|
+
? `Command \`${arg}\`: Permission denied by user.`
|
|
1150
|
+
: `${tag} ${arg}: Permission denied by user.`;
|
|
1151
|
+
logToolCall(permDesc.tag, { args: call.slice(1) }, false, 'denied');
|
|
1152
|
+
results.push(resultStr);
|
|
1153
|
+
if (debugEntries) debugEntries.push({ tag, call, ms: 0, status: 'denied', exitCode: null, result: resultStr });
|
|
1154
|
+
aborted = true;
|
|
1155
|
+
break;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1039
1158
|
|
|
1040
|
-
|
|
1159
|
+
const toolStart = Date.now();
|
|
1160
|
+
const invocationId = `tool-${iteration}-${invocationCounter++}-${tag}`;
|
|
1161
|
+
const startCtx = { id: invocationId, call, attrs, startedAt: toolStart };
|
|
1041
1162
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1163
|
+
if (cb.onToolStart) cb.onToolStart(tag, arg, startCtx);
|
|
1164
|
+
|
|
1165
|
+
try {
|
|
1166
|
+
if (tag === 'shell') {
|
|
1167
|
+
const shellResult = await agentExecShell(arg, { signal: controller.signal });
|
|
1168
|
+
const ms = Date.now() - toolStart;
|
|
1169
|
+
if (shellResult.aborted) {
|
|
1170
|
+
// User pressed Ctrl+C mid-command. The child process tree
|
|
1171
|
+
// has already been terminated by killTreeEscalating in
|
|
1172
|
+
// tools.js. Surface a clear message to the model so it can
|
|
1173
|
+
// plan around the interruption instead of blindly retrying
|
|
1174
|
+
// the same long-running command on the next turn.
|
|
1175
|
+
const elapsedS = shellResult.elapsed_s || 0;
|
|
1176
|
+
const oneLine = String(arg).replace(/\s+/g, ' ').trim();
|
|
1177
|
+
const truncatedCmd = oneLine.length > 80 ? oneLine.slice(0, 77) + '...' : oneLine;
|
|
1178
|
+
const resultStr = `User interrupted execution after ${elapsedS}s. Tool was running: ${truncatedCmd}. Plan around this — do not retry the same long-running command.`;
|
|
1179
|
+
if (cb.onToolEnd) cb.onToolEnd(tag, resultStr, ms, { id: invocationId, call, attrs, meta: null, error: { message: 'aborted' } });
|
|
1180
|
+
results.push(resultStr);
|
|
1181
|
+
if (debugEntries) debugEntries.push({ tag, call, ms, status: 'aborted', exitCode: null, result: resultStr });
|
|
1182
|
+
aborted = true;
|
|
1183
|
+
break;
|
|
1184
|
+
} else {
|
|
1185
|
+
let out = shellResult.stdout;
|
|
1186
|
+
if (shellResult.stderr) out += `\nSTDERR: ${shellResult.stderr}`;
|
|
1187
|
+
const resultStr = `Command \`${arg}\`:\nExit code: ${shellResult.exit_code}\n${out}`;
|
|
1188
|
+
const meta = _metaForTool(tag, shellResult);
|
|
1189
|
+
const error = shellResult.exit_code !== 0
|
|
1190
|
+
? { message: `exit ${shellResult.exit_code}`, code: shellResult.exit_code }
|
|
1191
|
+
: null;
|
|
1192
|
+
if (cb.onToolEnd) cb.onToolEnd(tag, resultStr, ms, { id: invocationId, call, attrs, meta, error });
|
|
1193
|
+
results.push(resultStr);
|
|
1194
|
+
if (debugEntries) debugEntries.push({
|
|
1195
|
+
tag,
|
|
1196
|
+
call,
|
|
1197
|
+
ms,
|
|
1198
|
+
status: shellResult.exit_code === 0 ? 'ok' : 'nonzero_exit',
|
|
1199
|
+
exitCode: shellResult.exit_code,
|
|
1200
|
+
result: resultStr,
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
continue;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
const fileResult = await agentExecFile(...call, { signal: controller.signal });
|
|
1045
1207
|
const ms = Date.now() - toolStart;
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1208
|
+
|
|
1209
|
+
if (fileResult.aborted) {
|
|
1210
|
+
// User pressed Ctrl+C while a file/network tool was running.
|
|
1211
|
+
// The per-tool abort listener has already torn down the in-flight
|
|
1212
|
+
// op (closed the FS read, destroyed the HTTP request, stopped the
|
|
1213
|
+
// recursive walk). Surface a clear note to the model so the next
|
|
1214
|
+
// turn doesn't replay the same long-running operation.
|
|
1215
|
+
const elapsedS = fileResult.elapsed_s || 0;
|
|
1216
|
+
const oneLine = String(arg).replace(/\s+/g, ' ').trim();
|
|
1217
|
+
const truncatedArg = oneLine.length > 80 ? oneLine.slice(0, 77) + '...' : oneLine;
|
|
1218
|
+
const resultStr = `User interrupted execution after ${elapsedS}s. Tool was running: ${tag} ${truncatedArg}. Plan around this — do not retry the same long-running operation.`;
|
|
1219
|
+
if (cb.onToolEnd) cb.onToolEnd(tag, resultStr, ms, { id: invocationId, call, attrs, meta: null, error: { message: 'aborted' } });
|
|
1049
1220
|
results.push(resultStr);
|
|
1050
|
-
if (debugEntries) debugEntries.push({ tag, call, ms, status: '
|
|
1221
|
+
if (debugEntries) debugEntries.push({ tag, call, ms, status: 'aborted', exitCode: null, result: resultStr });
|
|
1051
1222
|
aborted = true;
|
|
1052
1223
|
break;
|
|
1053
1224
|
} else {
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
const
|
|
1057
|
-
|
|
1058
|
-
const error = shellResult.exit_code !== 0
|
|
1059
|
-
? { message: `exit ${shellResult.exit_code}`, code: shellResult.exit_code }
|
|
1225
|
+
const resultStr = formatFileResult(call, fileResult);
|
|
1226
|
+
const meta = _metaForTool(tag, fileResult);
|
|
1227
|
+
const error = fileResult.error
|
|
1228
|
+
? { message: fileResult.error, code: fileResult.error_code || null }
|
|
1060
1229
|
: null;
|
|
1061
1230
|
if (cb.onToolEnd) cb.onToolEnd(tag, resultStr, ms, { id: invocationId, call, attrs, meta, error });
|
|
1062
1231
|
results.push(resultStr);
|
|
@@ -1064,53 +1233,26 @@ function createAgentRunner({ chatStream, extractToolCalls, agentExecShell, agent
|
|
|
1064
1233
|
tag,
|
|
1065
1234
|
call,
|
|
1066
1235
|
ms,
|
|
1067
|
-
status:
|
|
1068
|
-
exitCode:
|
|
1236
|
+
status: fileResult.error ? 'error' : 'ok',
|
|
1237
|
+
exitCode: null,
|
|
1069
1238
|
result: resultStr,
|
|
1070
1239
|
});
|
|
1071
1240
|
}
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
results.push(
|
|
1082
|
-
if (debugEntries) debugEntries.push({ tag, call, ms, status: '
|
|
1083
|
-
aborted = true;
|
|
1084
|
-
break;
|
|
1085
|
-
} else {
|
|
1086
|
-
const resultStr = formatFileResult(call, fileResult);
|
|
1087
|
-
const meta = _metaForTool(tag, fileResult);
|
|
1088
|
-
const error = fileResult.error
|
|
1089
|
-
? { message: fileResult.error, code: fileResult.error_code || null }
|
|
1090
|
-
: null;
|
|
1091
|
-
if (cb.onToolEnd) cb.onToolEnd(tag, resultStr, ms, { id: invocationId, call, attrs, meta, error });
|
|
1092
|
-
results.push(resultStr);
|
|
1093
|
-
if (debugEntries) debugEntries.push({
|
|
1094
|
-
tag,
|
|
1095
|
-
call,
|
|
1096
|
-
ms,
|
|
1097
|
-
status: fileResult.error ? 'error' : 'ok',
|
|
1098
|
-
exitCode: null,
|
|
1099
|
-
result: resultStr,
|
|
1100
|
-
});
|
|
1101
|
-
}
|
|
1102
|
-
} catch (err) {
|
|
1103
|
-
const ms = Date.now() - toolStart;
|
|
1104
|
-
if (cb.onToolEnd) cb.onToolEnd(tag, `Error: ${err.message}`, ms, { id: invocationId, call, attrs, meta: null, error: err });
|
|
1105
|
-
if (cb.onError) {
|
|
1106
|
-
cb.onError({ message: `Tool error (${tag}): ${err.message}`, isWarning: true });
|
|
1107
|
-
} else {
|
|
1108
|
-
messages.toolError(tag, err.message);
|
|
1241
|
+
} catch (err) {
|
|
1242
|
+
const ms = Date.now() - toolStart;
|
|
1243
|
+
if (cb.onToolEnd) cb.onToolEnd(tag, `Error: ${err.message}`, ms, { id: invocationId, call, attrs, meta: null, error: err });
|
|
1244
|
+
if (cb.onError) {
|
|
1245
|
+
cb.onError({ message: `Tool error (${tag}): ${err.message}`, isWarning: true });
|
|
1246
|
+
} else {
|
|
1247
|
+
messages.toolError(tag, err.message);
|
|
1248
|
+
}
|
|
1249
|
+
logToolCall(tag, { args: call.slice(1) }, false, 'error');
|
|
1250
|
+
results.push(`${tag}: Error — ${err.message}`);
|
|
1251
|
+
if (debugEntries) debugEntries.push({ tag, call, ms, status: 'exception', exitCode: null, result: `Error — ${err.message}` });
|
|
1109
1252
|
}
|
|
1110
|
-
logToolCall(tag, { args: call.slice(1) }, false, 'error');
|
|
1111
|
-
results.push(`${tag}: Error — ${err.message}`);
|
|
1112
|
-
if (debugEntries) debugEntries.push({ tag, call, ms, status: 'exception', exitCode: null, result: `Error — ${err.message}` });
|
|
1113
1253
|
}
|
|
1254
|
+
} finally {
|
|
1255
|
+
clearInterval(toolAbortWatcher);
|
|
1114
1256
|
}
|
|
1115
1257
|
|
|
1116
1258
|
if (debug && debugEntries && debugEntries.length > 0) {
|
|
@@ -1167,9 +1309,14 @@ function createAgentRunner({ chatStream, extractToolCalls, agentExecShell, agent
|
|
|
1167
1309
|
} else {
|
|
1168
1310
|
messages.sysWarn(warnMsg);
|
|
1169
1311
|
}
|
|
1170
|
-
// Push whatever results accumulated before the
|
|
1171
|
-
// context if the user asks to continue.
|
|
1312
|
+
// Push whatever results accumulated before the stop so the LLM has
|
|
1313
|
+
// context if the user asks to continue. The reason matters: an abort
|
|
1314
|
+
// (Ctrl+C) and a denial are both surfaced through the same `aborted`
|
|
1315
|
+
// flag, but the model should know which happened so it doesn't
|
|
1316
|
+
// immediately retry a runaway command after the user explicitly
|
|
1317
|
+
// killed it.
|
|
1172
1318
|
if (results.length > 0) {
|
|
1319
|
+
const reason = isAborted() ? 'user interrupted' : 'after user denied an action';
|
|
1173
1320
|
if (isNativeCall) {
|
|
1174
1321
|
for (let i = 0; i < results.length; i++) {
|
|
1175
1322
|
messages.push({ role: 'tool', tool_call_id: nativeToolCallIds[i], content: results[i] });
|
|
@@ -1177,7 +1324,7 @@ function createAgentRunner({ chatStream, extractToolCalls, agentExecShell, agent
|
|
|
1177
1324
|
} else {
|
|
1178
1325
|
messages.push({
|
|
1179
1326
|
role: 'user',
|
|
1180
|
-
content: `Tool execution results (partial — stopped
|
|
1327
|
+
content: `Tool execution results (partial — stopped: ${reason}):\n\n${results.join('\n\n')}`,
|
|
1181
1328
|
});
|
|
1182
1329
|
}
|
|
1183
1330
|
}
|