@semalt-ai/code 1.8.1 → 1.8.3
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 +14 -1
- package/CLAUDE.md +2 -1
- package/index.js +15 -1
- package/lib/agent.js +582 -121
- package/lib/api.js +182 -25
- package/lib/commands.js +57 -80
- package/lib/config.js +32 -4
- package/lib/constants.js +51 -1
- package/lib/metrics.js +16 -3
- package/lib/permissions.js +66 -67
- package/lib/prompts.js +93 -86
- package/lib/tool_specs.js +499 -0
- package/lib/tools.js +405 -192
- package/lib/ui/ansi.js +13 -1
- package/lib/ui/chat-history.js +201 -61
- package/lib/ui/create-ui.js +116 -373
- package/lib/ui/diff.js +87 -75
- package/lib/ui/input-field.js +75 -57
- package/lib/ui/status-bar.js +53 -23
- package/lib/ui/terminal.js +58 -0
- package/lib/ui/theme.js +78 -0
- package/lib/ui/utils.js +63 -1
- package/lib/ui/writer.js +255 -0
- package/lib/ui.js +5 -0
- package/package.json +1 -1
package/lib/constants.js
CHANGED
|
@@ -17,18 +17,34 @@ const DEFAULT_CONFIG = {
|
|
|
17
17
|
temperature: 0.7,
|
|
18
18
|
request_timeout_ms: DEFAULT_API_TIMEOUT_MS,
|
|
19
19
|
stream: true,
|
|
20
|
+
// native_tools (boolean, default true): when true, the
|
|
21
|
+
// client sends an OpenAI-format `tools` parameter and
|
|
22
|
+
// expects structured tool_calls in responses. Set to
|
|
23
|
+
// false only for models/endpoints that do not support
|
|
24
|
+
// native function calling (legacy finetunes, XML-only
|
|
25
|
+
// adapters). Per-profile flag on models[] entries.
|
|
20
26
|
models: [],
|
|
21
27
|
theme: 'dark',
|
|
22
28
|
max_file_size_kb: 512,
|
|
23
29
|
command_timeout_ms: 30000,
|
|
24
30
|
max_output_lines: 50,
|
|
31
|
+
http_fetch_max_bytes: 262144,
|
|
25
32
|
show_token_count: true,
|
|
26
33
|
show_cost: false,
|
|
27
34
|
system_prompt_mode: 'system_role',
|
|
35
|
+
repair_malformed_tool_xml: false,
|
|
28
36
|
};
|
|
29
37
|
|
|
30
38
|
const CONFIG_PATH = path.join(os.homedir(), '.semalt-ai', 'config.json');
|
|
31
39
|
|
|
40
|
+
// TAG_REGISTRY classifies every XML tag the stream parser may encounter.
|
|
41
|
+
// For 'tool'-type tags, the *parameter schema* lives in lib/tool_specs.js
|
|
42
|
+
// (TOOL_SPECS) — that file is the single source of truth for argument
|
|
43
|
+
// names, types, required flags, and descriptions used to build the
|
|
44
|
+
// native function-calling `tools` array and the system-prompt tag
|
|
45
|
+
// inventory. Adding or renaming a 'tool' entry here requires a matching
|
|
46
|
+
// change in TOOL_SPECS; the assertion at the bottom of this module
|
|
47
|
+
// enforces that parity at load time.
|
|
32
48
|
const TAG_REGISTRY = {
|
|
33
49
|
// Rendered visually in chat, never shown as raw text
|
|
34
50
|
think: { type: 'visual', streaming: true, display: 'think_bubble' },
|
|
@@ -59,7 +75,6 @@ const TAG_REGISTRY = {
|
|
|
59
75
|
download: { type: 'tool', streaming: false, label: 'Downloading' },
|
|
60
76
|
upload: { type: 'tool', streaming: false, label: 'Uploading' },
|
|
61
77
|
http_get: { type: 'tool', streaming: false, label: 'Fetching URL' },
|
|
62
|
-
http_get_next: { type: 'tool', streaming: false, label: 'Fetching next content chunk' },
|
|
63
78
|
ask_user: { type: 'tool', streaming: false, label: 'Asking user' },
|
|
64
79
|
store_memory: { type: 'tool', streaming: false, label: 'Storing memory' },
|
|
65
80
|
recall_memory: { type: 'tool', streaming: false, label: 'Recalling memory' },
|
|
@@ -81,6 +96,14 @@ const TAG_REGISTRY = {
|
|
|
81
96
|
tool_call: { type: 'tool', streaming: false, label: 'Using tool' },
|
|
82
97
|
function_call: { type: 'tool', streaming: false, label: 'Using tool' },
|
|
83
98
|
|
|
99
|
+
// Qwen3-Coder / Qwen3.5 XML tool-call format: `<function=tool_name>…</function>`.
|
|
100
|
+
// The tool name is carried as an `=name` suffix on the opening tag rather
|
|
101
|
+
// than an attribute; `parameter` (already registered as `strip` above) covers
|
|
102
|
+
// the matching `<parameter=key>…</parameter>` child tags. StreamParser splits
|
|
103
|
+
// the tag name on `[\s=]`, so the registry lookup for `<function=read_file>`
|
|
104
|
+
// resolves here.
|
|
105
|
+
function: { type: 'tool', streaming: false, label: 'Using tool' },
|
|
106
|
+
|
|
84
107
|
// Silently stripped — model wrapper artifacts
|
|
85
108
|
answer: { type: 'strip' },
|
|
86
109
|
response: { type: 'strip' },
|
|
@@ -90,8 +113,35 @@ const TAG_REGISTRY = {
|
|
|
90
113
|
text: { type: 'strip' },
|
|
91
114
|
result: { type: 'strip' },
|
|
92
115
|
code: { type: 'strip' },
|
|
116
|
+
|
|
117
|
+
// Protocol wrapper: the model's declared final reply to the user. Tags are
|
|
118
|
+
// stripped from rendered output but the inner content IS the user-facing
|
|
119
|
+
// answer and must stream through onToken, not be buffered like tool blocks.
|
|
120
|
+
final_answer: { type: 'final', streaming: true, label: 'Final answer' },
|
|
93
121
|
};
|
|
94
122
|
|
|
123
|
+
// Load-time parity check: every 'tool'-type tag in TAG_REGISTRY must have a
|
|
124
|
+
// matching entry in TOOL_SPECS, and TOOL_SPECS must not declare phantom
|
|
125
|
+
// tools that aren't registered. Requiring tool_specs.js here (rather than
|
|
126
|
+
// at the top of the file) keeps the module boundary one-directional —
|
|
127
|
+
// tool_specs.js does not depend on this file.
|
|
128
|
+
const { TOOL_SPECS } = require('./tool_specs');
|
|
129
|
+
(function assertToolSpecParity() {
|
|
130
|
+
const registryTools = Object.entries(TAG_REGISTRY)
|
|
131
|
+
.filter(([, v]) => v.type === 'tool')
|
|
132
|
+
.map(([k]) => k)
|
|
133
|
+
.sort();
|
|
134
|
+
const specTools = Object.keys(TOOL_SPECS).sort();
|
|
135
|
+
const missing = registryTools.filter((k) => !Object.prototype.hasOwnProperty.call(TOOL_SPECS, k));
|
|
136
|
+
const extra = specTools.filter((k) => !(k in TAG_REGISTRY) || TAG_REGISTRY[k].type !== 'tool');
|
|
137
|
+
if (missing.length || extra.length) {
|
|
138
|
+
const parts = [];
|
|
139
|
+
if (missing.length) parts.push(`missing in TOOL_SPECS: ${missing.join(', ')}`);
|
|
140
|
+
if (extra.length) parts.push(`extra in TOOL_SPECS: ${extra.join(', ')}`);
|
|
141
|
+
throw new Error(`TAG_REGISTRY ↔ TOOL_SPECS mismatch — ${parts.join('; ')}`);
|
|
142
|
+
}
|
|
143
|
+
})();
|
|
144
|
+
|
|
95
145
|
module.exports = {
|
|
96
146
|
CONFIG_PATH,
|
|
97
147
|
DEFAULT_API_TIMEOUT_MS,
|
package/lib/metrics.js
CHANGED
|
@@ -32,13 +32,22 @@ class Metrics {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
tokenLimitStatus() {
|
|
35
|
-
if (this.modelTokenLimit === null) return null;
|
|
36
35
|
const used = this.contextTokens();
|
|
36
|
+
if (this.modelTokenLimit == null) {
|
|
37
|
+
// No known limit — still expose `used` once we have a turn's prompt_tokens
|
|
38
|
+
// so the UI can render "N tok · limit unknown" instead of hiding the line.
|
|
39
|
+
if (!this.turns.length || !used) return null;
|
|
40
|
+
return { used, limit: null, pct: null, bar: null };
|
|
41
|
+
}
|
|
37
42
|
const pct = Math.round((used / this.modelTokenLimit) * 100);
|
|
38
43
|
const bar = this._buildBar(pct, 10);
|
|
39
44
|
return { used, limit: this.modelTokenLimit, pct, bar };
|
|
40
45
|
}
|
|
41
46
|
|
|
47
|
+
setModelTokenLimit(limit) {
|
|
48
|
+
this.modelTokenLimit = Number.isInteger(limit) && limit > 0 ? limit : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
42
51
|
_buildBar(pct, width) {
|
|
43
52
|
const filled = Math.min(Math.round((pct / 100) * width), width);
|
|
44
53
|
const empty = Math.max(0, width - filled);
|
|
@@ -79,8 +88,12 @@ class Metrics {
|
|
|
79
88
|
|
|
80
89
|
const status = this.tokenLimitStatus();
|
|
81
90
|
if (status !== null) {
|
|
82
|
-
|
|
83
|
-
|
|
91
|
+
if (status.limit === null) {
|
|
92
|
+
lines.push(row(` Context used: ${status.used} (limit unknown)`));
|
|
93
|
+
} else {
|
|
94
|
+
lines.push(row(` Context used: ${this.contextTokens()}`));
|
|
95
|
+
lines.push(row(` Token limit: ${status.used}/${status.limit} (${status.pct}%)`));
|
|
96
|
+
}
|
|
84
97
|
}
|
|
85
98
|
|
|
86
99
|
lines.push(row(` Duration: ${durationStr}`));
|
package/lib/permissions.js
CHANGED
|
@@ -8,7 +8,6 @@ const TIER_SYS = ['system_info', 'get_env', 'set_env'];
|
|
|
8
8
|
const TIER_MAP = { fs: TIER_FS, exec: TIER_EXEC, net: TIER_NET, sys: TIER_SYS };
|
|
9
9
|
const READONLY_BLOCKED = new Set(['write_file', 'append_file', 'delete_file', 'move_file', 'copy_file']);
|
|
10
10
|
|
|
11
|
-
let _permissionCounter = 0;
|
|
12
11
|
let _permissionQueueTail = Promise.resolve();
|
|
13
12
|
|
|
14
13
|
function createPermissionManager(ui, { allowedTiers = [], readonly = false } = {}) {
|
|
@@ -21,8 +20,8 @@ function createPermissionManager(ui, { allowedTiers = [], readonly = false } = {
|
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
const state = {
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
autoApproveAll: false,
|
|
24
|
+
sessionApprovedTags: new Set(),
|
|
26
25
|
};
|
|
27
26
|
|
|
28
27
|
let uiCallbacks = null;
|
|
@@ -40,92 +39,96 @@ function createPermissionManager(ui, { allowedTiers = [], readonly = false } = {
|
|
|
40
39
|
|
|
41
40
|
const MAX_DESC_LINES = 12;
|
|
42
41
|
|
|
43
|
-
|
|
42
|
+
// The picker renders into the writer's modal region — a live band above
|
|
43
|
+
// the status bar that redraws in place on every keystroke. Arrow-key
|
|
44
|
+
// navigation rebuilds the lines array and calls onShowModal again; nothing
|
|
45
|
+
// lands in scrollback until the user confirms. On resolve/cancel the
|
|
46
|
+
// modal is cleared and a single summary line is emitted to scrollback
|
|
47
|
+
// (for multi-line descriptions — e.g. a file-write diff — the full body
|
|
48
|
+
// is retained so the user can still see what was approved).
|
|
49
|
+
function requestPermission(description, onShowModal, onCloseModal, onCaptureNavigation) {
|
|
44
50
|
// Serialize dialogs: each permission waits for the previous one to be answered
|
|
45
51
|
const myTurn = _permissionQueueTail;
|
|
46
52
|
let releaseQueue;
|
|
47
53
|
_permissionQueueTail = new Promise((r) => { releaseQueue = r; });
|
|
48
54
|
|
|
49
55
|
return myTurn.then(() => new Promise((resolve) => {
|
|
50
|
-
const uniqueId = `perm_${++_permissionCounter}`;
|
|
51
56
|
const options = ['Yes', 'Always', 'No'];
|
|
52
57
|
let selectedIdx = 0;
|
|
53
58
|
const descLines = description.split('\n');
|
|
54
59
|
const truncatable = descLines.length > MAX_DESC_LINES;
|
|
55
60
|
|
|
56
|
-
function
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
function buildModalLines() {
|
|
62
|
+
const lines = [];
|
|
63
|
+
const visible = truncatable
|
|
64
|
+
? descLines.slice(0, MAX_DESC_LINES).concat([` \x1b[2m… ${descLines.length - MAX_DESC_LINES} more lines\x1b[0m`])
|
|
65
|
+
: descLines;
|
|
66
|
+
// First description line gets the bullet glyph; continuation lines
|
|
67
|
+
// are indented to align under it. Matches the pre-modal rendering
|
|
68
|
+
// that went through chatHistory's system-message renderer.
|
|
69
|
+
const first = visible[0] || '';
|
|
70
|
+
lines.push(` \x1b[38;5;244m●\x1b[0m \x1b[38;5;244m${first}\x1b[0m`);
|
|
71
|
+
for (let i = 1; i < visible.length; i++) {
|
|
72
|
+
lines.push(` \x1b[38;5;244m${visible[i]}\x1b[0m`);
|
|
63
73
|
}
|
|
64
|
-
|
|
74
|
+
lines.push('');
|
|
65
75
|
for (let i = 0; i < options.length; i++) {
|
|
66
|
-
|
|
76
|
+
lines.push(i === selectedIdx
|
|
67
77
|
? `\x1b[1m\x1b[36m ► ${options[i]}\x1b[0m`
|
|
68
78
|
: ` ${options[i]}`
|
|
69
79
|
);
|
|
70
80
|
}
|
|
71
|
-
return
|
|
81
|
+
return lines;
|
|
72
82
|
}
|
|
73
83
|
|
|
74
|
-
|
|
75
|
-
|
|
84
|
+
onShowModal(buildModalLines());
|
|
85
|
+
|
|
86
|
+
function finish(result) {
|
|
87
|
+
const chosen = result === 'cancel' ? 'no' : options[selectedIdx].toLowerCase();
|
|
88
|
+
const glyph = (chosen === 'no') ? '✗' : '✓';
|
|
89
|
+
// The full `description` is preserved in the summary so multi-line
|
|
90
|
+
// bodies (e.g. file-write diffs) remain visible in scrollback after
|
|
91
|
+
// the modal closes. chatHistory's system-message renderer styles the
|
|
92
|
+
// first line by the leading glyph and indents continuations.
|
|
93
|
+
onCloseModal(`${glyph} ${description}`);
|
|
94
|
+
releaseQueue();
|
|
95
|
+
resolve(chosen);
|
|
96
|
+
}
|
|
76
97
|
|
|
77
98
|
const releaseNav = onCaptureNavigation((action) => {
|
|
78
99
|
if (action === 'next') {
|
|
79
100
|
selectedIdx = (selectedIdx + 1) % options.length;
|
|
80
|
-
|
|
81
|
-
onRerenderMessage(uniqueId);
|
|
101
|
+
onShowModal(buildModalLines());
|
|
82
102
|
} else if (action === 'prev') {
|
|
83
103
|
selectedIdx = (selectedIdx - 1 + options.length) % options.length;
|
|
84
|
-
|
|
85
|
-
onRerenderMessage(uniqueId);
|
|
104
|
+
onShowModal(buildModalLines());
|
|
86
105
|
} else if (action === 'select') {
|
|
87
|
-
const chosen = options[selectedIdx];
|
|
88
106
|
releaseNav();
|
|
89
|
-
|
|
90
|
-
onCollapseMessage(uniqueId);
|
|
91
|
-
releaseQueue();
|
|
92
|
-
resolve(chosen.toLowerCase());
|
|
107
|
+
finish('select');
|
|
93
108
|
} else if (action === 'cancel') {
|
|
94
109
|
releaseNav();
|
|
95
|
-
|
|
96
|
-
onCollapseMessage(uniqueId);
|
|
97
|
-
releaseQueue();
|
|
98
|
-
resolve('no');
|
|
110
|
+
finish('cancel');
|
|
99
111
|
}
|
|
100
112
|
});
|
|
101
113
|
}));
|
|
102
114
|
}
|
|
103
115
|
|
|
104
|
-
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
console.log(` ${FG_GREEN}✓${RST} ${FG_DARK}Auto-approved: ${description}${RST}`);
|
|
110
|
-
}
|
|
111
|
-
return true;
|
|
116
|
+
function _emitAutoApproved(description) {
|
|
117
|
+
if (uiCallbacks) {
|
|
118
|
+
uiCallbacks.onAddMessage({ role: 'system', content: `✓ Auto-approved: ${description}` });
|
|
119
|
+
} else {
|
|
120
|
+
console.log(` ${FG_GREEN}✓${RST} ${FG_DARK}Auto-approved: ${description}${RST}`);
|
|
112
121
|
}
|
|
122
|
+
}
|
|
113
123
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
} else {
|
|
118
|
-
console.log(` ${FG_GREEN}✓${RST} ${FG_DARK}Auto-approved: ${description}${RST}`);
|
|
119
|
-
}
|
|
124
|
+
async function askPermission(actionType, description, tag) {
|
|
125
|
+
if (state.autoApproveAll) {
|
|
126
|
+
_emitAutoApproved(description);
|
|
120
127
|
return true;
|
|
121
128
|
}
|
|
122
129
|
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
uiCallbacks.onAddMessage({ role: 'system', content: `✓ Auto-approved: ${description}` });
|
|
126
|
-
} else {
|
|
127
|
-
console.log(` ${FG_GREEN}✓${RST} ${FG_DARK}Auto-approved: ${description}${RST}`);
|
|
128
|
-
}
|
|
130
|
+
if (tag && (autoApprovedTags.has(tag) || state.sessionApprovedTags.has(tag))) {
|
|
131
|
+
_emitAutoApproved(description);
|
|
129
132
|
return true;
|
|
130
133
|
}
|
|
131
134
|
|
|
@@ -137,25 +140,23 @@ function createPermissionManager(ui, { allowedTiers = [], readonly = false } = {
|
|
|
137
140
|
if (uiCallbacks) {
|
|
138
141
|
const result = await requestPermission(
|
|
139
142
|
`${actionType}: ${description}`,
|
|
140
|
-
uiCallbacks.
|
|
141
|
-
uiCallbacks.
|
|
142
|
-
uiCallbacks.onCollapseMessage,
|
|
143
|
+
uiCallbacks.onShowModal,
|
|
144
|
+
uiCallbacks.onCloseModal,
|
|
143
145
|
uiCallbacks.onCaptureNavigation,
|
|
144
146
|
);
|
|
145
147
|
|
|
146
148
|
if (result === 'no') return false;
|
|
147
149
|
|
|
148
|
-
if (result === 'always') {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
uiCallbacks.onAddMessage({ role: 'system', content: `✓ Auto-approve enabled for ${actionType} operations` });
|
|
150
|
+
if (result === 'always' && tag) {
|
|
151
|
+
state.sessionApprovedTags.add(tag);
|
|
152
|
+
uiCallbacks.onAddMessage({ role: 'system', content: `✓ Auto-approve enabled for \`${tag}\` this session` });
|
|
152
153
|
}
|
|
153
154
|
|
|
154
155
|
return true;
|
|
155
156
|
}
|
|
156
157
|
|
|
157
158
|
// Fallback: legacy TTY interactive select (used outside of chat UI)
|
|
158
|
-
const alwaysLabel =
|
|
159
|
+
const alwaysLabel = tag ? `Yes, always for <${tag}>` : 'Yes, always';
|
|
159
160
|
const choices = ['Yes', alwaysLabel, 'No'];
|
|
160
161
|
|
|
161
162
|
console.log();
|
|
@@ -177,10 +178,9 @@ function createPermissionManager(ui, { allowedTiers = [], readonly = false } = {
|
|
|
177
178
|
return false;
|
|
178
179
|
}
|
|
179
180
|
|
|
180
|
-
if (selectedIndex === 1) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
console.log(` ${FG_GREEN}✓${RST} ${FG_DARK}Auto-approve enabled for ${actionType} operations${RST}`);
|
|
181
|
+
if (selectedIndex === 1 && tag) {
|
|
182
|
+
state.sessionApprovedTags.add(tag);
|
|
183
|
+
console.log(` ${FG_GREEN}✓${RST} ${FG_DARK}Auto-approve enabled for <${tag}> this session${RST}`);
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
return true;
|
|
@@ -194,14 +194,13 @@ function createPermissionManager(ui, { allowedTiers = [], readonly = false } = {
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
function clear() {
|
|
197
|
-
state.
|
|
198
|
-
state.
|
|
197
|
+
state.autoApproveAll = false;
|
|
198
|
+
state.sessionApprovedTags.clear();
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
function toggleAll() {
|
|
202
|
-
state.
|
|
203
|
-
|
|
204
|
-
return state.autoApproveShell;
|
|
202
|
+
state.autoApproveAll = !state.autoApproveAll;
|
|
203
|
+
return state.autoApproveAll;
|
|
205
204
|
}
|
|
206
205
|
|
|
207
206
|
return {
|
package/lib/prompts.js
CHANGED
|
@@ -1,112 +1,119 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
<replace_in_file path="/path/to/file" search="old pattern" replace="new text"></replace_in_file>
|
|
48
|
-
|
|
49
|
-
To replace a specific line in a file:
|
|
50
|
-
<edit_file path="/path/to/file" line="42">replacement line content</edit_file>
|
|
51
|
-
|
|
52
|
-
To fetch a URL over HTTP or HTTPS (HTML pages are auto-converted to plain text):
|
|
53
|
-
<http_get url="https://example.com/api/data"></http_get>
|
|
54
|
-
|
|
55
|
-
To fetch raw HTML when you need to parse markup, extract links, or inspect structure:
|
|
56
|
-
<http_get url="https://example.com/" raw="true"></http_get>
|
|
57
|
-
|
|
58
|
-
If the response is large it will be delivered in numbered parts. To retrieve the next part:
|
|
59
|
-
<http_get_next key="https://example.com/api/data"/>
|
|
60
|
-
|
|
61
|
-
To download a file from a URL (saved to current directory):
|
|
62
|
-
<download>https://example.com/file.zip</download>
|
|
63
|
-
|
|
64
|
-
To write base64-encoded content to a file:
|
|
65
|
-
<upload path="/path/to/file">base64encodedcontent</upload>
|
|
66
|
-
|
|
67
|
-
To ask the user a question and receive their typed answer:
|
|
68
|
-
<ask_user question="Which directory should I use?"></ask_user>
|
|
3
|
+
const { TAG_REGISTRY } = require('./constants');
|
|
4
|
+
|
|
5
|
+
const WRAPPER_NAMES = new Set([
|
|
6
|
+
'minimax:tool_call',
|
|
7
|
+
'qwen:tool_call',
|
|
8
|
+
'invoke',
|
|
9
|
+
'parameter',
|
|
10
|
+
'tool_call',
|
|
11
|
+
'function_call',
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
// For each tool tag: required attributes and a one-line purpose.
|
|
15
|
+
// Required attributes are derived from the matchers in `extractToolCalls`
|
|
16
|
+
// (lib/tools.js). Where a tag accepts either an attribute or inline content,
|
|
17
|
+
// the attribute is marked optional.
|
|
18
|
+
const TOOL_TAG_SPECS = {
|
|
19
|
+
exec: { attrs: [], purpose: 'Run a shell command (inline content).' },
|
|
20
|
+
shell: { attrs: [], purpose: 'Run a shell command (inline content).' },
|
|
21
|
+
read_file: { attrs: ['path?'], purpose: 'Read a file (path attr or inline content).' },
|
|
22
|
+
write_file: { attrs: ['path'], purpose: 'Write file with inline content (overwrites).' },
|
|
23
|
+
create_file: { attrs: ['path'], purpose: 'Create file with inline content.' },
|
|
24
|
+
append_file: { attrs: ['path'], purpose: 'Append inline content to file.' },
|
|
25
|
+
delete_file: { attrs: [], purpose: 'Delete a file (inline content = path).' },
|
|
26
|
+
list_dir: { attrs: [], purpose: 'List directory contents (inline content = path).' },
|
|
27
|
+
make_dir: { attrs: [], purpose: 'Create directory recursively (inline content = path).' },
|
|
28
|
+
remove_dir: { attrs: [], purpose: 'Remove directory recursively (inline content = path).' },
|
|
29
|
+
move_file: { attrs: ['src', 'dst'], purpose: 'Move or rename a file.' },
|
|
30
|
+
copy_file: { attrs: ['src', 'dst'], purpose: 'Copy a file.' },
|
|
31
|
+
file_stat: { attrs: [], purpose: 'Stat a file (inline content = path).' },
|
|
32
|
+
edit_file: { attrs: ['path', 'line'], purpose: 'Replace a single line in a file (inline content = new line).' },
|
|
33
|
+
search_files: { attrs: ['pattern?', 'dir?'], purpose: 'Find files by glob pattern.' },
|
|
34
|
+
search_in_file: { attrs: ['path'], purpose: 'Regex search inside a file (inline content = pattern).' },
|
|
35
|
+
replace_in_file: { attrs: ['path', 'search', 'replace'], purpose: 'Regex replace inside a file.' },
|
|
36
|
+
get_env: { attrs: [], purpose: 'Read an env var (inline content = name).' },
|
|
37
|
+
set_env: { attrs: ['name', 'value'], purpose: 'Set an env var for this process.' },
|
|
38
|
+
download: { attrs: [], purpose: 'HTTP download to the CWD (inline content = URL).' },
|
|
39
|
+
upload: { attrs: ['path'], purpose: 'Write base64-encoded content to file.' },
|
|
40
|
+
http_get: { attrs: ['url'], purpose: 'HTTP GET; returns the response body (truncated to a byte cap with an explicit notice when oversized).' },
|
|
41
|
+
ask_user: { attrs: ['question'], purpose: 'Ask the user a question and receive an answer.' },
|
|
42
|
+
store_memory: { attrs: ['key'], purpose: 'Persist a key/value to local memory (inline content = value).' },
|
|
43
|
+
recall_memory: { attrs: ['key'], purpose: 'Read a key from local memory.' },
|
|
44
|
+
list_memories: { attrs: [], purpose: 'List memory keys.' },
|
|
45
|
+
system_info: { attrs: [], purpose: 'Return platform, arch, host, memory, node version, cwd.' },
|
|
46
|
+
};
|
|
69
47
|
|
|
70
|
-
|
|
71
|
-
|
|
48
|
+
function buildTagInventory() {
|
|
49
|
+
const lines = [];
|
|
50
|
+
for (const tag of Object.keys(TAG_REGISTRY)) {
|
|
51
|
+
const entry = TAG_REGISTRY[tag];
|
|
52
|
+
if (entry.type !== 'tool') continue;
|
|
53
|
+
if (WRAPPER_NAMES.has(tag)) continue;
|
|
54
|
+
const spec = TOOL_TAG_SPECS[tag] || { attrs: [], purpose: '' };
|
|
55
|
+
const attrPart = spec.attrs.length
|
|
56
|
+
? ` [attrs: ${spec.attrs.join(', ')}]`
|
|
57
|
+
: '';
|
|
58
|
+
lines.push(`- <${tag}>${attrPart} — ${spec.purpose}`);
|
|
59
|
+
}
|
|
60
|
+
return lines.join('\n');
|
|
61
|
+
}
|
|
72
62
|
|
|
73
|
-
|
|
74
|
-
<recall_memory key="project_name"></recall_memory>
|
|
63
|
+
const TAG_INVENTORY = buildTagInventory();
|
|
75
64
|
|
|
76
|
-
|
|
77
|
-
<list_memories></list_memories>
|
|
65
|
+
const SYSTEM_PROMPT_TEMPLATE = `You are Semalt.AI, an expert AI coding assistant running in the user's terminal. You have the ability to execute shell commands and file operations.
|
|
78
66
|
|
|
79
|
-
|
|
80
|
-
<system_info></system_info>
|
|
67
|
+
## Available tool tags:
|
|
81
68
|
|
|
82
|
-
|
|
83
|
-
<get_env>VARIABLE_NAME</get_env>
|
|
69
|
+
${TAG_INVENTORY}
|
|
84
70
|
|
|
85
|
-
|
|
86
|
-
<set_env name="VARIABLE_NAME" value="value"/>
|
|
71
|
+
## Tool call syntax — use EXACTLY these forms:
|
|
87
72
|
|
|
88
|
-
|
|
89
|
-
|
|
73
|
+
- Single-value inline-content tags: put the value directly between the tags as plain text. **Do NOT nest pseudo-tags like \`<path>\`, \`<command>\`, \`<url>\`, \`<key>\`, \`<name>\`, \`<pattern>\`, or \`<question>\` inside the body** — the parser treats the body as the literal value.
|
|
74
|
+
- Correct: \`<list_dir>/tmp/foo</list_dir>\`, \`<shell>ls -la</shell>\`, \`<read_file>/etc/hosts</read_file>\`, \`<download>https://x.com/f.zip</download>\`
|
|
75
|
+
- Wrong: \`<list_dir><path>/tmp/foo</path></list_dir>\`, \`<shell><command>ls</command></shell>\`
|
|
76
|
+
- Attribute tags: parameters go as quoted attributes on the opening tag; the body is either empty (self-closed) or real payload content (e.g. file bytes for \`write_file\`).
|
|
77
|
+
- Correct: \`<write_file path="/tmp/a.txt">actual file contents here</write_file>\`, \`<http_get url="https://example.com"/>\`, \`<move_file src="/a" dst="/b"/>\`
|
|
78
|
+
- Wrong: \`<write_file><path>/tmp/a.txt</path><content>...</content></write_file>\`
|
|
90
79
|
|
|
91
80
|
## Reasoning vs planning — IMPORTANT:
|
|
92
81
|
|
|
93
82
|
- Your internal chain-of-thought reasoning uses your native \`<think>...</think>\` block. Use it normally for deliberation. Do NOT treat \`<think>\` as a user-facing tool and do NOT try to emit \`<think>\` as an action — it is reserved for your own reasoning and is handled by the runtime.
|
|
94
83
|
- When you need to explicitly record a short plan that the agent framework can see (for logging or hand-off between steps), use \`<plan>...</plan>\` instead. \`<plan>\` is a tool tag; \`<think>\` is not.
|
|
95
|
-
- Never emit \`<think>\` as
|
|
84
|
+
- Never emit \`<think>\` as an action. The valid action tags are the ones listed above.
|
|
96
85
|
|
|
97
86
|
## STRICT RULES — follow exactly:
|
|
98
87
|
|
|
99
|
-
1.
|
|
88
|
+
1. Emit tool tags as needed. If a step depends on a prior tool's result, wait for the result before the next call. Independent tool calls may be batched in a single response.
|
|
100
89
|
2. **Always end your response with exactly one action tag** whenever the task is not yet complete. Do NOT end a response with only prose like "Let me create the files…" — that prose must be immediately followed by the actual tool tag in the same response. Preamble without a tool tag is a bug.
|
|
101
90
|
3. Only use the exact tag names listed above. Unknown tag names will not be executed.
|
|
102
91
|
4. When the user asks you to do something on their system, USE the tools — do NOT just print instructions.
|
|
103
92
|
5. Each action will be shown to the user for approval before execution.
|
|
104
93
|
6. After execution you will receive the result and can proceed with the next action in a new response.
|
|
105
94
|
7. Be concise. Provide working solutions. Use markdown for code blocks in explanations.
|
|
106
|
-
8.
|
|
107
|
-
|
|
108
|
-
|
|
95
|
+
8. Current working directory: __CWD__
|
|
96
|
+
|
|
97
|
+
Response contract (strict):
|
|
98
|
+
Every response must end with exactly one of:
|
|
99
|
+
(a) a tool call, using one of the XML tags listed above; OR
|
|
100
|
+
(b) <final_answer>...</final_answer> containing your answer to the user.
|
|
101
|
+
A response containing neither is invalid and will be rejected.
|
|
102
|
+
Do not describe actions in prose outside these two forms. Either call the tool, or wrap your final reply in <final_answer>.`;
|
|
103
|
+
|
|
104
|
+
const NATIVE_SYSTEM_PROMPT_TEMPLATE = `You are Semalt.AI, an expert AI coding assistant running in the user's terminal. Use the provided tools to execute shell commands and file operations; do not just print instructions. Each call is approved by the user before execution, and the result is returned to you for the next step.
|
|
105
|
+
|
|
106
|
+
Use \`<think>...</think>\` for internal reasoning (runtime-handled; never emit as an action). Use \`<plan>...</plan>\` to record a short plan for the agent framework.
|
|
107
|
+
|
|
108
|
+
Be concise. Use markdown for code blocks in explanations. Current working directory: __CWD__
|
|
109
|
+
|
|
110
|
+
Response contract: every response must end with either (a) one or more tool calls, or (b) <final_answer>...</final_answer> containing your answer to the user. Prose outside those two forms is invalid.`;
|
|
111
|
+
|
|
112
|
+
function getSystemPrompt(nativeTools = false) {
|
|
113
|
+
const template = nativeTools ? NATIVE_SYSTEM_PROMPT_TEMPLATE : SYSTEM_PROMPT_TEMPLATE;
|
|
114
|
+
return template.replace('__CWD__', process.cwd());
|
|
115
|
+
}
|
|
109
116
|
|
|
110
117
|
module.exports = {
|
|
111
|
-
|
|
118
|
+
getSystemPrompt,
|
|
112
119
|
};
|