nox-qwen-agent-cli 1.1.5 → 1.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -14
- package/agent-system-prompt.js +6 -0
- package/extensions/nox-qwen-agent.vsix +0 -0
- package/package.json +1 -1
- package/qwen.js +229 -5
- 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
|
-
Interactive
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
Interactive runs always ask for a masked API key, including `qwen -p` when a
|
|
26
|
+
terminal is attached. Non-interactive runs read `NOX_API_KEY`,
|
|
27
|
+
`OPENAI_COMPAT_API_KEY`, or `OPENAI_API_KEY`. The key remains in memory for that
|
|
28
|
+
process only and is 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.
|
|
@@ -56,6 +60,7 @@ Clarification rules:
|
|
|
56
60
|
|
|
57
61
|
Progress rules:
|
|
58
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.
|
|
59
64
|
|
|
60
65
|
Safety rules:
|
|
61
66
|
- Stay inside the configured workspace root.
|
|
@@ -64,6 +69,7 @@ Safety rules:
|
|
|
64
69
|
- Do not read protected secret files unless explicitly authorised.
|
|
65
70
|
- Do not execute destructive commands without explicit permission.
|
|
66
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.
|
|
67
73
|
- Do not bypass tests merely to produce a successful-looking result.
|
|
68
74
|
|
|
69
75
|
Completion rules:
|
|
Binary file
|
package/package.json
CHANGED
package/qwen.js
CHANGED
|
@@ -19,6 +19,8 @@ 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;
|
|
22
24
|
const CLARIFY_REQUEST_PROMPT = `Clarify this request before execution.
|
|
23
25
|
Do not modify files, run commands with side effects, or implement the task yet.
|
|
24
26
|
Ask the minimum set of precise questions needed to remove ambiguity, identify acceptance criteria, and confirm risky choices.
|
|
@@ -40,6 +42,7 @@ When a tool is required, output exactly one block and no other text:
|
|
|
40
42
|
<NOX_TOOL_CALL>{"id":"short-stable-id","tool":"exact_tool_name","args":{}}</NOX_TOOL_CALL>
|
|
41
43
|
Arguments must match the selected tool schema. Never emit more than one tool call per response.
|
|
42
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.`;
|
|
43
46
|
|
|
44
47
|
const TOOL_DEFINITIONS = [
|
|
45
48
|
tool("update_plan", "Create or update the visible execution plan.", {
|
|
@@ -152,6 +155,19 @@ const TOOL_DEFINITIONS = [
|
|
|
152
155
|
cwd: stringProperty("Relative working directory; defaults to ."),
|
|
153
156
|
timeout_ms: integerProperty("Timeout from 1 to 600000 ms"),
|
|
154
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"]),
|
|
155
171
|
];
|
|
156
172
|
|
|
157
173
|
const EFFECTIVE_TOOL_DEFINITIONS = [
|
|
@@ -199,6 +215,57 @@ function refreshSystemPrompt(state) {
|
|
|
199
215
|
else state.messages.unshift({ role: "system", content: prompt });
|
|
200
216
|
}
|
|
201
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
|
+
|
|
202
269
|
function tool(name, description, properties, required = []) {
|
|
203
270
|
return {
|
|
204
271
|
type: "function",
|
|
@@ -685,6 +752,9 @@ async function executeToolCall(call, context) {
|
|
|
685
752
|
}
|
|
686
753
|
return await runShell(String(args.command), cwd, args.timeout_ms);
|
|
687
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);
|
|
688
758
|
return { ok: false, error: `Unknown tool: ${name}` };
|
|
689
759
|
} catch (error) {
|
|
690
760
|
return { ok: false, error: String(error?.message || error) };
|
|
@@ -853,6 +923,125 @@ async function runShell(command, cwd, timeoutValue) {
|
|
|
853
923
|
});
|
|
854
924
|
}
|
|
855
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
|
+
|
|
856
1045
|
async function requestCompletion(state) {
|
|
857
1046
|
for (let attempt = 0; attempt <= MAX_API_RETRIES; attempt += 1) {
|
|
858
1047
|
try {
|
|
@@ -872,9 +1061,10 @@ async function requestCompletion(state) {
|
|
|
872
1061
|
|
|
873
1062
|
async function requestCompletionOnce(state) {
|
|
874
1063
|
const providerSession = state.providerSessions?.[state.model] || null;
|
|
1064
|
+
const prompt = systemPrompt(state.workspace, state.options);
|
|
875
1065
|
const body = {
|
|
876
1066
|
model: state.model,
|
|
877
|
-
messages: state
|
|
1067
|
+
messages: outboundMessages(state),
|
|
878
1068
|
tools: EFFECTIVE_TOOL_DEFINITIONS,
|
|
879
1069
|
tool_choice: "auto",
|
|
880
1070
|
parallel_tool_calls: false,
|
|
@@ -886,6 +1076,8 @@ async function requestCompletionOnce(state) {
|
|
|
886
1076
|
nox_new_provider_chat: !providerSession,
|
|
887
1077
|
...(providerSession ? { nox_provider_session: providerSession } : {}),
|
|
888
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"),
|
|
889
1081
|
nox_thinking_level: "medium",
|
|
890
1082
|
},
|
|
891
1083
|
};
|
|
@@ -929,9 +1121,26 @@ async function collectChatStream(body, { onText, onThinking }) {
|
|
|
929
1121
|
let buffer = "";
|
|
930
1122
|
let content = "";
|
|
931
1123
|
let reasoning = "";
|
|
1124
|
+
let pendingText = "";
|
|
1125
|
+
let streamingText = false;
|
|
932
1126
|
const toolCalls = [];
|
|
933
1127
|
let providerSession = null;
|
|
934
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
|
+
};
|
|
935
1144
|
while (true) {
|
|
936
1145
|
const { value, done } = await reader.read();
|
|
937
1146
|
if (done) break;
|
|
@@ -966,7 +1175,7 @@ async function collectChatStream(body, { onText, onThinking }) {
|
|
|
966
1175
|
}
|
|
967
1176
|
if (typeof delta.content === "string" && delta.content) {
|
|
968
1177
|
content += delta.content;
|
|
969
|
-
|
|
1178
|
+
handleText(delta.content);
|
|
970
1179
|
}
|
|
971
1180
|
for (const part of delta.tool_calls || []) {
|
|
972
1181
|
const index = Number(part.index || 0);
|
|
@@ -988,6 +1197,17 @@ async function collectChatStream(body, { onText, onThinking }) {
|
|
|
988
1197
|
error.partialResponse = Boolean(content || toolCalls.length);
|
|
989
1198
|
throw error;
|
|
990
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);
|
|
991
1211
|
if (content && !onText) process.stdout.write("\n");
|
|
992
1212
|
return {
|
|
993
1213
|
role: "assistant",
|
|
@@ -1004,7 +1224,7 @@ async function runAgentTurn(state, userMessage) {
|
|
|
1004
1224
|
await persistSession(state);
|
|
1005
1225
|
for (let step = 0; step < MAX_TOOL_STEPS; step += 1) {
|
|
1006
1226
|
state.ui?.working(step + 1);
|
|
1007
|
-
const assistant = await requestCompletion(state);
|
|
1227
|
+
const assistant = normalizeAssistantToolTransport(await requestCompletion(state));
|
|
1008
1228
|
if (assistant.provider_session && typeof assistant.provider_session === "object") {
|
|
1009
1229
|
state.providerSessions[state.model] = assistant.provider_session;
|
|
1010
1230
|
}
|
|
@@ -1123,6 +1343,7 @@ async function main() {
|
|
|
1123
1343
|
workspace,
|
|
1124
1344
|
sessionId: "",
|
|
1125
1345
|
providerSessions: {},
|
|
1346
|
+
backgroundTerminals: new Map(),
|
|
1126
1347
|
messages: [],
|
|
1127
1348
|
ui,
|
|
1128
1349
|
historyErrorShown: false,
|
|
@@ -1153,7 +1374,7 @@ async function main() {
|
|
|
1153
1374
|
|
|
1154
1375
|
try {
|
|
1155
1376
|
if (options.prompt) {
|
|
1156
|
-
await runAgentTurn(state, { role: "user", content: options.prompt });
|
|
1377
|
+
await runAgentTurn(state, { role: "user", content: options.clarify ? clarifyPrompt(options.prompt) : options.prompt });
|
|
1157
1378
|
return;
|
|
1158
1379
|
}
|
|
1159
1380
|
if (!ui) throw new Error("Interactive mode requires a terminal or --prompt.");
|
|
@@ -1203,13 +1424,16 @@ async function main() {
|
|
|
1203
1424
|
const image = parseImageCommand(input);
|
|
1204
1425
|
const file = parseFileCommand(input);
|
|
1205
1426
|
const planTask = input.startsWith("/plan ") ? input.slice(6).trim() : "";
|
|
1427
|
+
const clarifyTask = input.startsWith("/clarify ") ? input.slice(9).trim() : "";
|
|
1206
1428
|
try {
|
|
1207
1429
|
await runAgentTurn(state,
|
|
1208
1430
|
image ? await imageMessage(image.filePath, image.prompt)
|
|
1209
1431
|
: file ? await fileMessage(file.filePath, file.prompt)
|
|
1210
1432
|
: {
|
|
1211
1433
|
role: "user",
|
|
1212
|
-
content:
|
|
1434
|
+
content: clarifyTask
|
|
1435
|
+
? clarifyPrompt(clarifyTask)
|
|
1436
|
+
: planTask
|
|
1213
1437
|
? `Create and show a concrete plan with update_plan before executing this task, then keep it updated:\n\n${planTask}`
|
|
1214
1438
|
: input,
|
|
1215
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() {
|