nox-qwen-agent-cli 1.1.4 → 1.1.6
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/README.md +27 -14
- package/agent-system-prompt.js +13 -0
- package/extensions/nox-qwen-agent.vsix +0 -0
- package/package.json +1 -1
- package/qwen.js +285 -6
- package/terminal-ui.js +1 -1
package/README.md
CHANGED
|
@@ -22,10 +22,10 @@ PowerShell can also run `.\install.ps1` directly.
|
|
|
22
22
|
|
|
23
23
|
## Configure
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
Runs read `NOX_API_KEY`, `OPENAI_COMPAT_API_KEY`, or `OPENAI_API_KEY` when one
|
|
26
|
+
is configured. If no key is configured and a terminal is available, the CLI asks
|
|
27
|
+
for a masked API key. The key remains in memory for that process only and is
|
|
28
|
+
never stored in source code, settings, or chat history.
|
|
29
29
|
|
|
30
30
|
Linux, macOS, and Termux:
|
|
31
31
|
|
|
@@ -45,6 +45,7 @@ $env:NOX_API_KEY = "your-key"
|
|
|
45
45
|
qwen
|
|
46
46
|
qwen -m qwen3.7-plus
|
|
47
47
|
qwen -m gpt-5.5 -p "Inspect and test this project."
|
|
48
|
+
qwen --clarify -p "Add authentication."
|
|
48
49
|
qwen --yes --no-stream
|
|
49
50
|
```
|
|
50
51
|
|
|
@@ -56,8 +57,13 @@ The interactive composer is multiline:
|
|
|
56
57
|
- Arrow keys, Home, End, Backspace, and Delete edit the message.
|
|
57
58
|
- Multiline clipboard paste is preserved.
|
|
58
59
|
|
|
59
|
-
Commands: `/plan TASK`, `/image PATH PROMPT`, `/file PATH PROMPT`,
|
|
60
|
-
`/resume SESSION`, `/approval ask|auto`, `/model`, and `/exit`.
|
|
60
|
+
Commands: `/plan TASK`, `/clarify TASK`, `/image PATH PROMPT`, `/file PATH PROMPT`,
|
|
61
|
+
`/new`, `/sessions`, `/resume SESSION`, `/approval ask|auto`, `/model`, and `/exit`.
|
|
62
|
+
|
|
63
|
+
Use `/clarify TASK` or `--clarify -p "TASK"` when you want the agent to ask
|
|
64
|
+
precise requirements questions first. In clarification mode the agent should
|
|
65
|
+
not edit files or run side-effecting commands; it returns focused questions and
|
|
66
|
+
safe assumptions before implementation.
|
|
61
67
|
|
|
62
68
|
Images and files are sent as structured OpenAI attachment parts. Their bytes
|
|
63
69
|
are uploaded through the selected provider's file endpoint and are never
|
|
@@ -67,8 +73,9 @@ Streaming is enabled by default. Reasoning is shown in a separate Thinking
|
|
|
67
73
|
section when the provider returns it, and is never mixed into the final answer.
|
|
68
74
|
|
|
69
75
|
The agent includes bounded file reads, batch reads and writes, tree and text
|
|
70
|
-
search, path metadata, file operations, Git status/diff,
|
|
71
|
-
|
|
76
|
+
search, path metadata, file operations, Git status/diff, cross-platform shell
|
|
77
|
+
execution, and background terminals for dev servers and long-running commands.
|
|
78
|
+
Mutating actions require approval unless `--yes` is used.
|
|
72
79
|
For substantial tasks the model can call `update_plan`; plan steps and their
|
|
73
80
|
live status are rendered separately from chat and tool output.
|
|
74
81
|
|
|
@@ -77,12 +84,18 @@ live status are rendered separately from chat and tool output.
|
|
|
77
84
|
The Agent prompt and file tools are maintained centrally. Available workspace
|
|
78
85
|
tools include `file_info`, `list_files`, `search_files`, `find_in_file`,
|
|
79
86
|
`read_file`, `read_lines`, `update_file`, `apply_patch`, `create_file`,
|
|
80
|
-
`delete_file`, `move_file`, `copy_file`,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
87
|
+
`delete_file`, `move_file`, `copy_file`, `make_directory`,
|
|
88
|
+
`start_background_terminal`, `read_background_terminal`, and
|
|
89
|
+
`stop_background_terminal`.
|
|
90
|
+
|
|
91
|
+
Every Agent run sends fresh runtime context in the first user message:
|
|
92
|
+
OS/version, processor, Node.js version, workspace root, top-level workspace
|
|
93
|
+
entries, available tools, and approval mode. The CLI and VS Code extension
|
|
94
|
+
intentionally transport the full Agent instructions by prefixing the first user
|
|
95
|
+
message with `<agent_instructions>`. Every fourth user turn is prefixed with
|
|
96
|
+
`<agent_reminder>` so the model keeps the active workspace and agent role in
|
|
97
|
+
context. This avoids gateways that drop or weaken `system` messages while still
|
|
98
|
+
sending a SHA-256 prompt proof in request metadata.
|
|
86
99
|
|
|
87
100
|
The file tools enforce workspace-only paths, reject traversal and symlink
|
|
88
101
|
escapes, deny protected secret-looking files by default, detect binary files,
|
package/agent-system-prompt.js
CHANGED
|
@@ -28,9 +28,13 @@ Tool-selection rules:
|
|
|
28
28
|
- Use create_file only when a new file is genuinely required.
|
|
29
29
|
- Use delete_file only when deletion is explicitly required and safe.
|
|
30
30
|
- Use run_shell_command only for commands relevant to the user's task.
|
|
31
|
+
- Use start_background_terminal for long-running commands such as dev servers, watchers, and commands that need later polling.
|
|
32
|
+
- Use read_background_terminal to inspect background output before claiming a server, watcher, or long task is ready.
|
|
33
|
+
- Use stop_background_terminal when a background process is no longer needed or is producing failures.
|
|
31
34
|
- Run focused tests first, followed by the broader suite when appropriate.
|
|
32
35
|
|
|
33
36
|
File-access rules:
|
|
37
|
+
- A file may be available through an uploaded file id, an attachment in the current request, a client-provided excerpt, workspace tools, a line-range read, or a search result followed by a targeted read.
|
|
34
38
|
- Do not ask the user to upload a file when it is already available in the workspace.
|
|
35
39
|
- Do not read an entire large file when a targeted range is sufficient.
|
|
36
40
|
- Do not invent missing file contents.
|
|
@@ -47,8 +51,16 @@ Editing rules:
|
|
|
47
51
|
Planning rules:
|
|
48
52
|
Use a plan when the task has multiple steps, affects several files, requires investigation, or requires tests.
|
|
49
53
|
|
|
54
|
+
Clarification rules:
|
|
55
|
+
- If a request is broad, ambiguous, risky, destructive, or missing acceptance criteria, ask concise clarifying questions before changing files or running commands with side effects.
|
|
56
|
+
- Ask only for details that materially affect the implementation, validation, or safety of the task.
|
|
57
|
+
- Keep clarification questions numbered, specific, and limited to five unless the user explicitly asks for a deeper requirements interview.
|
|
58
|
+
- When a reasonable default is safe, state the assumption and continue instead of blocking.
|
|
59
|
+
- If the user explicitly asks for clarification mode, do not execute the task yet; return the questions and the proposed assumptions only.
|
|
60
|
+
|
|
50
61
|
Progress rules:
|
|
51
62
|
For longer operations, provide brief progress updates after meaningful milestones. Do not narrate every low-level command.
|
|
63
|
+
When a background terminal is running, summarize only meaningful state changes such as started, ready, failed, or stopped.
|
|
52
64
|
|
|
53
65
|
Safety rules:
|
|
54
66
|
- Stay inside the configured workspace root.
|
|
@@ -57,6 +69,7 @@ Safety rules:
|
|
|
57
69
|
- Do not read protected secret files unless explicitly authorised.
|
|
58
70
|
- Do not execute destructive commands without explicit permission.
|
|
59
71
|
- Do not install untrusted packages or extensions silently.
|
|
72
|
+
- Do not leave background terminals running after they are no longer needed unless the user asked to keep them running.
|
|
60
73
|
- Do not bypass tests merely to produce a successful-looking result.
|
|
61
74
|
|
|
62
75
|
Completion rules:
|
|
Binary file
|
package/package.json
CHANGED
package/qwen.js
CHANGED
|
@@ -19,6 +19,16 @@ const MAX_FILE_BYTES = 20 * 1024 * 1024;
|
|
|
19
19
|
const MAX_COMMAND_OUTPUT = 128 * 1024;
|
|
20
20
|
const MAX_API_RETRIES = 2;
|
|
21
21
|
const MAX_SHELL_TIMEOUT_MS = 10 * 60 * 1000;
|
|
22
|
+
const MAX_BACKGROUND_OUTPUT = 256 * 1024;
|
|
23
|
+
const MAX_BACKGROUND_TERMINALS = 8;
|
|
24
|
+
const CLARIFY_REQUEST_PROMPT = `Clarify this request before execution.
|
|
25
|
+
Do not modify files, run commands with side effects, or implement the task yet.
|
|
26
|
+
Ask the minimum set of precise questions needed to remove ambiguity, identify acceptance criteria, and confirm risky choices.
|
|
27
|
+
Use no more than five numbered questions unless the user explicitly asks for a deeper requirements interview.
|
|
28
|
+
If the request is already clear enough, say so and list the assumptions you would use before execution.
|
|
29
|
+
|
|
30
|
+
Request:
|
|
31
|
+
`;
|
|
22
32
|
const SYSTEM_PROMPT_RULES = `The tools attached to the request are authoritative. Use their exact names and JSON schemas.
|
|
23
33
|
Use tools for every local operation. Never claim a command ran or a file changed without a tool result.
|
|
24
34
|
Perform one coherent tool action at a time, inspect its result, and continue until the task is complete.
|
|
@@ -32,6 +42,7 @@ When a tool is required, output exactly one block and no other text:
|
|
|
32
42
|
<NOX_TOOL_CALL>{"id":"short-stable-id","tool":"exact_tool_name","args":{}}</NOX_TOOL_CALL>
|
|
33
43
|
Arguments must match the selected tool schema. Never emit more than one tool call per response.
|
|
34
44
|
After receiving a <tool_result> block, continue the same objective with the next tool call or the final answer.`;
|
|
45
|
+
const AGENT_REMINDER_PROMPT = `Agent reminder: continue following the initial Agent instructions, use the available tools when needed, keep workspace operations inside the active workspace, and finish only after the requested task is handled.`;
|
|
35
46
|
|
|
36
47
|
const TOOL_DEFINITIONS = [
|
|
37
48
|
tool("update_plan", "Create or update the visible execution plan.", {
|
|
@@ -144,6 +155,19 @@ const TOOL_DEFINITIONS = [
|
|
|
144
155
|
cwd: stringProperty("Relative working directory; defaults to ."),
|
|
145
156
|
timeout_ms: integerProperty("Timeout from 1 to 600000 ms"),
|
|
146
157
|
}, ["command"]),
|
|
158
|
+
tool("start_background_terminal", "Start a long-running shell command in the background and return a terminal id for later polling.", {
|
|
159
|
+
command: stringProperty("Shell command"),
|
|
160
|
+
cwd: stringProperty("Relative working directory; defaults to ."),
|
|
161
|
+
}, ["command"]),
|
|
162
|
+
tool("read_background_terminal", "Read buffered output and status from a background terminal.", {
|
|
163
|
+
terminal_id: stringProperty("Background terminal id returned by start_background_terminal"),
|
|
164
|
+
max_bytes: integerProperty("Maximum stdout/stderr bytes to return; defaults to 12000"),
|
|
165
|
+
wait_ms: integerProperty("Optional polling wait from 0 to 10000 ms for new output or process exit"),
|
|
166
|
+
}, ["terminal_id"]),
|
|
167
|
+
tool("stop_background_terminal", "Stop a running background terminal.", {
|
|
168
|
+
terminal_id: stringProperty("Background terminal id returned by start_background_terminal"),
|
|
169
|
+
signal: stringProperty("Signal name; defaults to SIGTERM"),
|
|
170
|
+
}, ["terminal_id"]),
|
|
147
171
|
];
|
|
148
172
|
|
|
149
173
|
const EFFECTIVE_TOOL_DEFINITIONS = [
|
|
@@ -191,6 +215,57 @@ function refreshSystemPrompt(state) {
|
|
|
191
215
|
else state.messages.unshift({ role: "system", content: prompt });
|
|
192
216
|
}
|
|
193
217
|
|
|
218
|
+
function prefixTextContent(content, prompt, kind) {
|
|
219
|
+
const firstTurn = kind === "instructions";
|
|
220
|
+
return [
|
|
221
|
+
firstTurn ? "<agent_instructions>" : "<agent_reminder>",
|
|
222
|
+
prompt,
|
|
223
|
+
firstTurn ? "</agent_instructions>" : "</agent_reminder>",
|
|
224
|
+
"<user_message>",
|
|
225
|
+
String(content ?? ""),
|
|
226
|
+
"</user_message>",
|
|
227
|
+
].join("\n");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function promptForUserTurn(userTurn, prompt) {
|
|
231
|
+
if (userTurn === 1) return { kind: "instructions", prompt };
|
|
232
|
+
if (userTurn > 1 && userTurn % 4 === 0) return { kind: "reminder", prompt: AGENT_REMINDER_PROMPT };
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function prefixUserContent(content, transport) {
|
|
237
|
+
if (!transport) return content;
|
|
238
|
+
if (Array.isArray(content)) {
|
|
239
|
+
let inserted = false;
|
|
240
|
+
const parts = content.map((part) => {
|
|
241
|
+
if (!inserted && part?.type === "text") {
|
|
242
|
+
inserted = true;
|
|
243
|
+
return { ...part, text: prefixTextContent(part.text, transport.prompt, transport.kind) };
|
|
244
|
+
}
|
|
245
|
+
return part;
|
|
246
|
+
});
|
|
247
|
+
if (inserted) return parts;
|
|
248
|
+
return [{ type: "text", text: prefixTextContent("", transport.prompt, transport.kind) }, ...parts];
|
|
249
|
+
}
|
|
250
|
+
return prefixTextContent(content, transport.prompt, transport.kind);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function outboundMessages(state) {
|
|
254
|
+
const prompt = systemPrompt(state.workspace, state.options);
|
|
255
|
+
let userTurn = 1;
|
|
256
|
+
return state.messages
|
|
257
|
+
.filter((message) => message?.role !== "system" && message?.role !== "developer")
|
|
258
|
+
.map((message) => {
|
|
259
|
+
if (message?.role !== "user") return message;
|
|
260
|
+
const transport = promptForUserTurn(userTurn, prompt);
|
|
261
|
+
userTurn += 1;
|
|
262
|
+
return {
|
|
263
|
+
...message,
|
|
264
|
+
content: prefixUserContent(message.content, transport),
|
|
265
|
+
};
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
194
269
|
function tool(name, description, properties, required = []) {
|
|
195
270
|
return {
|
|
196
271
|
type: "function",
|
|
@@ -226,6 +301,7 @@ function parseArgs(argv) {
|
|
|
226
301
|
prompt: "",
|
|
227
302
|
stream: true,
|
|
228
303
|
yes: false,
|
|
304
|
+
clarify: false,
|
|
229
305
|
approvalMode: "ask",
|
|
230
306
|
history: true,
|
|
231
307
|
resume: "",
|
|
@@ -238,6 +314,7 @@ function parseArgs(argv) {
|
|
|
238
314
|
else if (value === "-p" || value === "--prompt") options.prompt = argv[++index] || "";
|
|
239
315
|
else if (value === "--base-url") options.baseUrl = argv[++index] || options.baseUrl;
|
|
240
316
|
else if (value === "-C" || value === "--workspace") options.workspace = argv[++index] || options.workspace;
|
|
317
|
+
else if (value === "--clarify") options.clarify = true;
|
|
241
318
|
else if (value === "--no-stream") options.stream = false;
|
|
242
319
|
else if (value === "--stream") options.stream = true;
|
|
243
320
|
else if (value === "--request-timeout") options.requestTimeoutMs = Number.parseInt(argv[++index] || "", 10) * 1000;
|
|
@@ -262,11 +339,13 @@ Usage:
|
|
|
262
339
|
qwen
|
|
263
340
|
qwen install ext
|
|
264
341
|
qwen -m gpt-5.5 -p "inspect this project"
|
|
342
|
+
qwen --clarify -p "add auth"
|
|
265
343
|
qwen --yes --no-stream
|
|
266
344
|
|
|
267
345
|
Options:
|
|
268
346
|
-m, --model MODEL Select model without the menu
|
|
269
347
|
-p, --prompt TEXT Run one task and exit
|
|
348
|
+
--clarify Ask precise clarification questions for --prompt before execution
|
|
270
349
|
-C, --workspace PATH Active workspace
|
|
271
350
|
--base-url URL OpenAI-compatible base URL
|
|
272
351
|
--stream / --no-stream Streaming mode; streaming is default
|
|
@@ -287,10 +366,15 @@ Interactive commands:
|
|
|
287
366
|
/resume SESSION Resume a saved chat
|
|
288
367
|
/approval ask|auto Change approval mode for this session
|
|
289
368
|
/plan TASK Require a visible plan before execution
|
|
369
|
+
/clarify TASK Ask precise questions and assumptions before execution
|
|
290
370
|
/clear Alias for /new
|
|
291
371
|
/exit Exit`;
|
|
292
372
|
}
|
|
293
373
|
|
|
374
|
+
function clarifyPrompt(request) {
|
|
375
|
+
return `${CLARIFY_REQUEST_PROMPT}${String(request || "").trim()}`;
|
|
376
|
+
}
|
|
377
|
+
|
|
294
378
|
function configuredApiKey() {
|
|
295
379
|
return String(
|
|
296
380
|
process.env.NOX_API_KEY
|
|
@@ -668,6 +752,9 @@ async function executeToolCall(call, context) {
|
|
|
668
752
|
}
|
|
669
753
|
return await runShell(String(args.command), cwd, args.timeout_ms);
|
|
670
754
|
}
|
|
755
|
+
if (name === "start_background_terminal") return await startBackgroundTerminal(context, args);
|
|
756
|
+
if (name === "read_background_terminal") return await readBackgroundTerminal(context, args);
|
|
757
|
+
if (name === "stop_background_terminal") return await stopBackgroundTerminal(context, args);
|
|
671
758
|
return { ok: false, error: `Unknown tool: ${name}` };
|
|
672
759
|
} catch (error) {
|
|
673
760
|
return { ok: false, error: String(error?.message || error) };
|
|
@@ -764,6 +851,44 @@ function shellQuote(value) {
|
|
|
764
851
|
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
765
852
|
}
|
|
766
853
|
|
|
854
|
+
function parseTextToolCall(content) {
|
|
855
|
+
const text = typeof content === "string" ? content.trim() : "";
|
|
856
|
+
const match = text.match(/^<NOX_TOOL_CALL>([\s\S]+)<\/NOX_TOOL_CALL>$/);
|
|
857
|
+
if (!match) return null;
|
|
858
|
+
let payload;
|
|
859
|
+
try {
|
|
860
|
+
payload = JSON.parse(match[1]);
|
|
861
|
+
} catch {
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
const name = String(payload?.tool || "");
|
|
865
|
+
if (!name) return null;
|
|
866
|
+
const id = String(payload?.id || `text_tool_${crypto.randomUUID().slice(0, 8)}`);
|
|
867
|
+
const args = payload?.args && typeof payload.args === "object" && !Array.isArray(payload.args)
|
|
868
|
+
? payload.args
|
|
869
|
+
: {};
|
|
870
|
+
return {
|
|
871
|
+
id,
|
|
872
|
+
type: "function",
|
|
873
|
+
function: {
|
|
874
|
+
name,
|
|
875
|
+
arguments: JSON.stringify(args),
|
|
876
|
+
},
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function normalizeAssistantToolTransport(assistant) {
|
|
881
|
+
const nativeCalls = Array.isArray(assistant.tool_calls) ? assistant.tool_calls : [];
|
|
882
|
+
if (nativeCalls.length) return assistant;
|
|
883
|
+
const textCall = parseTextToolCall(assistant.content);
|
|
884
|
+
if (!textCall) return assistant;
|
|
885
|
+
return {
|
|
886
|
+
...assistant,
|
|
887
|
+
content: null,
|
|
888
|
+
tool_calls: [textCall],
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
|
|
767
892
|
async function runShell(command, cwd, timeoutValue) {
|
|
768
893
|
const timeoutMs = Math.max(1, Math.min(MAX_SHELL_TIMEOUT_MS, Number(timeoutValue || 120000)));
|
|
769
894
|
return await new Promise((resolvePromise) => {
|
|
@@ -798,6 +923,125 @@ async function runShell(command, cwd, timeoutValue) {
|
|
|
798
923
|
});
|
|
799
924
|
}
|
|
800
925
|
|
|
926
|
+
function ensureBackgroundTerminals(context) {
|
|
927
|
+
if (!context.backgroundTerminals) context.backgroundTerminals = new Map();
|
|
928
|
+
return context.backgroundTerminals;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function backgroundSnapshot(entry, maxBytes = 12000) {
|
|
932
|
+
const limit = Math.max(1, Math.min(MAX_BACKGROUND_OUTPUT, Number(maxBytes || 12000)));
|
|
933
|
+
const stdout = entry.stdout.slice(-limit);
|
|
934
|
+
const stderr = entry.stderr.slice(-limit);
|
|
935
|
+
return {
|
|
936
|
+
ok: true,
|
|
937
|
+
terminal_id: entry.id,
|
|
938
|
+
command: entry.command,
|
|
939
|
+
cwd: entry.relativeCwd,
|
|
940
|
+
running: entry.exit === null,
|
|
941
|
+
pid: entry.child.pid,
|
|
942
|
+
started_at: entry.startedAt,
|
|
943
|
+
exited_at: entry.exitedAt,
|
|
944
|
+
exit_code: entry.exit?.code ?? null,
|
|
945
|
+
signal: entry.exit?.signal ?? null,
|
|
946
|
+
stdout,
|
|
947
|
+
stderr,
|
|
948
|
+
truncated: entry.stdout.length > stdout.length || entry.stderr.length > stderr.length || entry.truncated,
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function appendBackgroundOutput(entry, target, chunk) {
|
|
953
|
+
const text = chunk.toString("utf8");
|
|
954
|
+
entry[target] += text;
|
|
955
|
+
if (entry.stdout.length + entry.stderr.length <= MAX_BACKGROUND_OUTPUT) return;
|
|
956
|
+
const overflow = entry.stdout.length + entry.stderr.length - MAX_BACKGROUND_OUTPUT;
|
|
957
|
+
if (entry[target].length >= overflow) entry[target] = entry[target].slice(overflow);
|
|
958
|
+
else {
|
|
959
|
+
const remaining = overflow - entry[target].length;
|
|
960
|
+
entry[target] = "";
|
|
961
|
+
const other = target === "stdout" ? "stderr" : "stdout";
|
|
962
|
+
entry[other] = entry[other].slice(Math.min(remaining, entry[other].length));
|
|
963
|
+
}
|
|
964
|
+
entry.truncated = true;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
async function startBackgroundTerminal(context, args) {
|
|
968
|
+
const terminals = ensureBackgroundTerminals(context);
|
|
969
|
+
for (const [id, entry] of terminals) {
|
|
970
|
+
if (entry.exit !== null) terminals.delete(id);
|
|
971
|
+
}
|
|
972
|
+
if (terminals.size >= MAX_BACKGROUND_TERMINALS) {
|
|
973
|
+
throw new Error(`At most ${MAX_BACKGROUND_TERMINALS} background terminals can run at once.`);
|
|
974
|
+
}
|
|
975
|
+
const cwd = safeWorkspacePath(context.workspace, args.cwd || ".");
|
|
976
|
+
const command = String(args.command || "");
|
|
977
|
+
if (!command.trim()) throw new Error("command must not be empty.");
|
|
978
|
+
if (!await confirm(context.terminal, context.options, "start background terminal", `$ ${command}\n cwd: ${path.relative(context.workspace, cwd) || "."}`)) {
|
|
979
|
+
return { ok: false, error: "User denied background terminal." };
|
|
980
|
+
}
|
|
981
|
+
const windows = process.platform === "win32";
|
|
982
|
+
const executable = windows ? (process.env.ComSpec || "cmd.exe") : "/bin/sh";
|
|
983
|
+
const shellArgs = windows ? ["/d", "/s", "/c", command] : ["-lc", command];
|
|
984
|
+
const child = spawn(executable, shellArgs, { cwd, env: process.env, windowsHide: true });
|
|
985
|
+
const id = `bg_${crypto.randomUUID().slice(0, 8)}`;
|
|
986
|
+
const entry = {
|
|
987
|
+
id,
|
|
988
|
+
command,
|
|
989
|
+
relativeCwd: path.relative(context.workspace, cwd) || ".",
|
|
990
|
+
child,
|
|
991
|
+
stdout: "",
|
|
992
|
+
stderr: "",
|
|
993
|
+
truncated: false,
|
|
994
|
+
startedAt: new Date().toISOString(),
|
|
995
|
+
exitedAt: null,
|
|
996
|
+
exit: null,
|
|
997
|
+
};
|
|
998
|
+
child.stdout.on("data", (chunk) => appendBackgroundOutput(entry, "stdout", chunk));
|
|
999
|
+
child.stderr.on("data", (chunk) => appendBackgroundOutput(entry, "stderr", chunk));
|
|
1000
|
+
child.on("error", (error) => {
|
|
1001
|
+
entry.stderr += error.message;
|
|
1002
|
+
entry.exit = { code: null, signal: "ERROR" };
|
|
1003
|
+
entry.exitedAt = new Date().toISOString();
|
|
1004
|
+
});
|
|
1005
|
+
child.on("close", (code, signal) => {
|
|
1006
|
+
entry.exit = { code, signal };
|
|
1007
|
+
entry.exitedAt = new Date().toISOString();
|
|
1008
|
+
});
|
|
1009
|
+
terminals.set(id, entry);
|
|
1010
|
+
return backgroundSnapshot(entry, args.max_bytes);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
async function readBackgroundTerminal(context, args) {
|
|
1014
|
+
const terminals = ensureBackgroundTerminals(context);
|
|
1015
|
+
const id = String(args.terminal_id || "");
|
|
1016
|
+
const entry = terminals.get(id);
|
|
1017
|
+
if (!entry) return { ok: false, error: `Unknown background terminal: ${id}` };
|
|
1018
|
+
const waitMs = Math.max(0, Math.min(10000, Number(args.wait_ms || 0)));
|
|
1019
|
+
if (waitMs > 0 && entry.exit === null) {
|
|
1020
|
+
const stdoutLength = entry.stdout.length;
|
|
1021
|
+
const stderrLength = entry.stderr.length;
|
|
1022
|
+
const deadline = Date.now() + waitMs;
|
|
1023
|
+
while (Date.now() < deadline && entry.exit === null && entry.stdout.length === stdoutLength && entry.stderr.length === stderrLength) {
|
|
1024
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(50, deadline - Date.now())));
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
const snapshot = backgroundSnapshot(entry, args.max_bytes);
|
|
1028
|
+
if (!snapshot.running) terminals.delete(id);
|
|
1029
|
+
return snapshot;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
async function stopBackgroundTerminal(context, args) {
|
|
1033
|
+
const terminals = ensureBackgroundTerminals(context);
|
|
1034
|
+
const id = String(args.terminal_id || "");
|
|
1035
|
+
const entry = terminals.get(id);
|
|
1036
|
+
if (!entry) return { ok: false, error: `Unknown background terminal: ${id}` };
|
|
1037
|
+
const signal = String(args.signal || "SIGTERM");
|
|
1038
|
+
if (entry.exit === null) entry.child.kill(signal);
|
|
1039
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
1040
|
+
const snapshot = backgroundSnapshot(entry, args.max_bytes);
|
|
1041
|
+
if (entry.exit !== null) terminals.delete(id);
|
|
1042
|
+
return snapshot;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
801
1045
|
async function requestCompletion(state) {
|
|
802
1046
|
for (let attempt = 0; attempt <= MAX_API_RETRIES; attempt += 1) {
|
|
803
1047
|
try {
|
|
@@ -817,9 +1061,10 @@ async function requestCompletion(state) {
|
|
|
817
1061
|
|
|
818
1062
|
async function requestCompletionOnce(state) {
|
|
819
1063
|
const providerSession = state.providerSessions?.[state.model] || null;
|
|
1064
|
+
const prompt = systemPrompt(state.workspace, state.options);
|
|
820
1065
|
const body = {
|
|
821
1066
|
model: state.model,
|
|
822
|
-
messages: state
|
|
1067
|
+
messages: outboundMessages(state),
|
|
823
1068
|
tools: EFFECTIVE_TOOL_DEFINITIONS,
|
|
824
1069
|
tool_choice: "auto",
|
|
825
1070
|
parallel_tool_calls: false,
|
|
@@ -831,6 +1076,8 @@ async function requestCompletionOnce(state) {
|
|
|
831
1076
|
nox_new_provider_chat: !providerSession,
|
|
832
1077
|
...(providerSession ? { nox_provider_session: providerSession } : {}),
|
|
833
1078
|
qwen_cli_session_id: state.sessionId,
|
|
1079
|
+
nox_prompt_transport: "user-prefix",
|
|
1080
|
+
nox_system_prompt_sha256: crypto.createHash("sha256").update(prompt).digest("hex"),
|
|
834
1081
|
nox_thinking_level: "medium",
|
|
835
1082
|
},
|
|
836
1083
|
};
|
|
@@ -874,9 +1121,26 @@ async function collectChatStream(body, { onText, onThinking }) {
|
|
|
874
1121
|
let buffer = "";
|
|
875
1122
|
let content = "";
|
|
876
1123
|
let reasoning = "";
|
|
1124
|
+
let pendingText = "";
|
|
1125
|
+
let streamingText = false;
|
|
877
1126
|
const toolCalls = [];
|
|
878
1127
|
let providerSession = null;
|
|
879
1128
|
let completed = false;
|
|
1129
|
+
const maybeTextToolPrefix = (value) => {
|
|
1130
|
+
const trimmed = String(value || "").trimStart();
|
|
1131
|
+
return "<NOX_TOOL_CALL>".startsWith(trimmed) || trimmed.startsWith("<NOX_TOOL_CALL>");
|
|
1132
|
+
};
|
|
1133
|
+
const handleText = (chunk) => {
|
|
1134
|
+
if (streamingText) {
|
|
1135
|
+
onText(chunk);
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
pendingText += chunk;
|
|
1139
|
+
if (maybeTextToolPrefix(pendingText)) return;
|
|
1140
|
+
streamingText = true;
|
|
1141
|
+
onText(pendingText);
|
|
1142
|
+
pendingText = "";
|
|
1143
|
+
};
|
|
880
1144
|
while (true) {
|
|
881
1145
|
const { value, done } = await reader.read();
|
|
882
1146
|
if (done) break;
|
|
@@ -911,7 +1175,7 @@ async function collectChatStream(body, { onText, onThinking }) {
|
|
|
911
1175
|
}
|
|
912
1176
|
if (typeof delta.content === "string" && delta.content) {
|
|
913
1177
|
content += delta.content;
|
|
914
|
-
|
|
1178
|
+
handleText(delta.content);
|
|
915
1179
|
}
|
|
916
1180
|
for (const part of delta.tool_calls || []) {
|
|
917
1181
|
const index = Number(part.index || 0);
|
|
@@ -933,6 +1197,17 @@ async function collectChatStream(body, { onText, onThinking }) {
|
|
|
933
1197
|
error.partialResponse = Boolean(content || toolCalls.length);
|
|
934
1198
|
throw error;
|
|
935
1199
|
}
|
|
1200
|
+
const textToolCall = parseTextToolCall(content);
|
|
1201
|
+
if (textToolCall && !toolCalls.length) {
|
|
1202
|
+
return {
|
|
1203
|
+
role: "assistant",
|
|
1204
|
+
content: null,
|
|
1205
|
+
...(reasoning ? { reasoning_content: reasoning } : {}),
|
|
1206
|
+
...(providerSession ? { provider_session: providerSession } : {}),
|
|
1207
|
+
tool_calls: [textToolCall],
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
if (!streamingText && pendingText) onText(pendingText);
|
|
936
1211
|
if (content && !onText) process.stdout.write("\n");
|
|
937
1212
|
return {
|
|
938
1213
|
role: "assistant",
|
|
@@ -949,7 +1224,7 @@ async function runAgentTurn(state, userMessage) {
|
|
|
949
1224
|
await persistSession(state);
|
|
950
1225
|
for (let step = 0; step < MAX_TOOL_STEPS; step += 1) {
|
|
951
1226
|
state.ui?.working(step + 1);
|
|
952
|
-
const assistant = await requestCompletion(state);
|
|
1227
|
+
const assistant = normalizeAssistantToolTransport(await requestCompletion(state));
|
|
953
1228
|
if (assistant.provider_session && typeof assistant.provider_session === "object") {
|
|
954
1229
|
state.providerSessions[state.model] = assistant.provider_session;
|
|
955
1230
|
}
|
|
@@ -1043,7 +1318,7 @@ async function main() {
|
|
|
1043
1318
|
}
|
|
1044
1319
|
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
1045
1320
|
const ui = interactive ? new TerminalUI(process.stdin, process.stdout) : null;
|
|
1046
|
-
const apiKey = ui ? await ui.readApiKey() :
|
|
1321
|
+
const apiKey = configuredApiKey() || (ui ? await ui.readApiKey() : "");
|
|
1047
1322
|
if (!apiKey) {
|
|
1048
1323
|
throw new Error("Set OPENAI_COMPAT_API_KEY, OPENAI_API_KEY, or NOX_API_KEY before running qwen.");
|
|
1049
1324
|
}
|
|
@@ -1068,6 +1343,7 @@ async function main() {
|
|
|
1068
1343
|
workspace,
|
|
1069
1344
|
sessionId: "",
|
|
1070
1345
|
providerSessions: {},
|
|
1346
|
+
backgroundTerminals: new Map(),
|
|
1071
1347
|
messages: [],
|
|
1072
1348
|
ui,
|
|
1073
1349
|
historyErrorShown: false,
|
|
@@ -1098,7 +1374,7 @@ async function main() {
|
|
|
1098
1374
|
|
|
1099
1375
|
try {
|
|
1100
1376
|
if (options.prompt) {
|
|
1101
|
-
await runAgentTurn(state, { role: "user", content: options.prompt });
|
|
1377
|
+
await runAgentTurn(state, { role: "user", content: options.clarify ? clarifyPrompt(options.prompt) : options.prompt });
|
|
1102
1378
|
return;
|
|
1103
1379
|
}
|
|
1104
1380
|
if (!ui) throw new Error("Interactive mode requires a terminal or --prompt.");
|
|
@@ -1148,13 +1424,16 @@ async function main() {
|
|
|
1148
1424
|
const image = parseImageCommand(input);
|
|
1149
1425
|
const file = parseFileCommand(input);
|
|
1150
1426
|
const planTask = input.startsWith("/plan ") ? input.slice(6).trim() : "";
|
|
1427
|
+
const clarifyTask = input.startsWith("/clarify ") ? input.slice(9).trim() : "";
|
|
1151
1428
|
try {
|
|
1152
1429
|
await runAgentTurn(state,
|
|
1153
1430
|
image ? await imageMessage(image.filePath, image.prompt)
|
|
1154
1431
|
: file ? await fileMessage(file.filePath, file.prompt)
|
|
1155
1432
|
: {
|
|
1156
1433
|
role: "user",
|
|
1157
|
-
content:
|
|
1434
|
+
content: clarifyTask
|
|
1435
|
+
? clarifyPrompt(clarifyTask)
|
|
1436
|
+
: planTask
|
|
1158
1437
|
? `Create and show a concrete plan with update_plan before executing this task, then keep it updated:\n\n${planTask}`
|
|
1159
1438
|
: input,
|
|
1160
1439
|
});
|
package/terminal-ui.js
CHANGED
|
@@ -218,7 +218,7 @@ export class TerminalUI {
|
|
|
218
218
|
if (approvalMode === "auto") {
|
|
219
219
|
this.write(`${this.yellow(" Auto approval is active: model-requested changes and commands run without prompts.")}\n`);
|
|
220
220
|
}
|
|
221
|
-
this.write(`${this.dim(" /plan /image /file /model /sessions /approval /exit")}\n`);
|
|
221
|
+
this.write(`${this.dim(" /plan /clarify /image /file /model /sessions /approval /exit")}\n`);
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
async readMessage() {
|